aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2022-11-22 01:44:39 +0100
committerdec05eba <dec05eba@protonmail.com>2022-11-22 01:44:39 +0100
commit89c41c1488854858e02ff6bd48a6518161fa05a5 (patch)
tree2161e26f342c4b2f9579b6521dc347a29e25fa6c
parent52bc7111147dd3e87e4bf0ae57241c2b81892f78 (diff)
Allow launching directly into 4chan thread
-rw-r--r--README.md4
-rw-r--r--include/QuickMedia.hpp11
-rw-r--r--plugins/Fourchan.hpp2
-rw-r--r--plugins/ImageBoard.hpp12
-rw-r--r--src/QuickMedia.cpp117
-rw-r--r--src/plugins/Fourchan.cpp173
6 files changed, 205 insertions, 114 deletions
diff --git a/README.md b/README.md
index 216994c..eeef6dc 100644
--- a/README.md
+++ b/README.md
@@ -6,9 +6,9 @@ Currently supported web services: `youtube`, `peertube`, `lbry`, `soundcloud`, `
QuickMedia also supports reading local manga and watching local anime, see [local manga](#local-manga) and [local anime](#local-anime)
## Usage
```
-usage: quickmedia [plugin] [--no-video] [--upscale-images] [--upscale-images-always] [--dir <directory>] [--instance <instance>] [-e <window>] [--video-max-height <height>] [youtube-url] [youtube-channel-url]
+usage: quickmedia [plugin|] [--no-video] [--upscale-images] [--upscale-images-always] [--dir <directory>] [--instance <instance>] [-e <window>] [--video-max-height <height>]
OPTIONS:
- plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, local-manga, local-anime, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, dramacool, file-manager or stdin
+ plugin|url The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, local-manga, local-anime, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, dramacool, file-manager or stdin. This can also be a youtube url, youtube channel url or a 4chan thread url
--no-video Only play audio when playing a video. Disabled by default
--upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default
--upscale-images-always Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default
diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp
index 675fa0e..0980b76 100644
--- a/include/QuickMedia.hpp
+++ b/include/QuickMedia.hpp
@@ -170,6 +170,13 @@ namespace QuickMedia {
FORCE
};
+ enum class LaunchUrlType {
+ NONE,
+ YOUTUBE_VIDEO,
+ YOUTUBE_CHANNEL,
+ FOURCHAN_THREAD
+ };
+
Display *disp;
mgl::Window window;
Matrix *matrix = nullptr;
@@ -226,8 +233,8 @@ namespace QuickMedia {
bool window_closed = false;
std::string pipe_selected_text;
std::filesystem::path file_manager_start_dir;
- std::string youtube_url;
- std::string youtube_channel_url;
+ std::string launch_url;
+ LaunchUrlType launch_url_type = LaunchUrlType::NONE;
std::unique_ptr<VideoPlayer> video_player;
bool use_youtube_dl = false;
int video_max_height = 0;
diff --git a/plugins/Fourchan.hpp b/plugins/Fourchan.hpp
index 83fb293..51fe880 100644
--- a/plugins/Fourchan.hpp
+++ b/plugins/Fourchan.hpp
@@ -60,6 +60,8 @@ namespace QuickMedia {
FourchanThreadPage(Program *program, std::string board_id, std::string thread_id, std::string pass_id) :
ImageBoardThreadPage(program, std::move(board_id), std::move(thread_id)), pass_id(std::move(pass_id)) {}
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+
PostResult post_comment(const std::string &captcha_id, const std::string &captcha_solution, const std::string &comment, const std::string &filepath = "") override;
const std::string& get_pass_id() override;
PluginResult request_captcha_challenge(ImageBoardCaptchaChallenge &challenge_response) override;
diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp
index b324ea9..e135532 100644
--- a/plugins/ImageBoard.hpp
+++ b/plugins/ImageBoard.hpp
@@ -30,15 +30,14 @@ namespace QuickMedia {
int ttl = 0;
};
- class ImageBoardThreadPage : public VideoPage {
+ class ImageBoardThreadPage : public LazyFetchPage {
public:
- ImageBoardThreadPage(Program *program, std::string board_id, std::string thread_id) : VideoPage(program, ""), board_id(std::move(board_id)), thread_id(std::move(thread_id)) {}
+ ImageBoardThreadPage(Program *program, std::string board_id, std::string thread_id) : LazyFetchPage(program), board_id(std::move(board_id)), thread_id(std::move(thread_id)) {}
const char* get_title() const override { return ""; }
PageTypez get_type() const override { return PageTypez::IMAGE_BOARD_THREAD; }
void copy_to_clipboard(const BodyItem *body_item) override;
- bool autoplay_next_item() override { return true; }
// If |filepath| is empty then no file is uploaded
virtual PostResult post_comment(const std::string &captcha_id, const std::string &captcha_solution, const std::string &comment, const std::string &filepath = "") = 0;
virtual const std::string& get_pass_id();
@@ -48,4 +47,11 @@ namespace QuickMedia {
const std::string board_id;
const std::string thread_id;
};
+
+ class ImageBoardVideoPage : public VideoPage {
+ public:
+ ImageBoardVideoPage(Program *program) : VideoPage(program, "") {}
+ const char* get_title() const override { return ""; }
+ bool autoplay_next_item() override { return true; }
+ };
} \ No newline at end of file
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 6f089be..2e7feff 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -317,9 +317,9 @@ namespace QuickMedia {
}
static void usage() {
- fprintf(stderr, "usage: quickmedia [plugin] [--no-video] [--upscale-images] [--upscale-images-always] [--dir <directory>] [--instance <instance>] [-e <window>] [--video-max-height <height>] [youtube-url] [youtube-channel-url]\n");
+ fprintf(stderr, "usage: quickmedia [plugin|url] [--no-video] [--upscale-images] [--upscale-images-always] [--dir <directory>] [--instance <instance>] [-e <window>] [--video-max-height <height>]\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, local-manga, local-anime, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, dramacool, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n");
+ fprintf(stderr, " plugin|url The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, local-manga, local-anime, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, dramacool, file-manager, stdin, pornhub, spankbang, xvideos or xhamster. This can also be a youtube url, youtube channel url or a 4chan thread url\n");
fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n");
fprintf(stderr, " --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default\n");
fprintf(stderr, " --upscale-images-always Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default\n");
@@ -368,6 +368,44 @@ namespace QuickMedia {
return true;
}
+ // |comment_id| is optional
+ static bool fourchan_extract_url(const std::string &url, std::string &board_id, std::string &thread_id, std::string &comment_id) {
+ size_t len = 10;
+ size_t index = url.find("4chan.org/");
+ if(index == std::string::npos) {
+ len = 13;
+ index = url.find("4channel.org/");
+ }
+
+ if(index == std::string::npos)
+ return false;
+
+ index += len;
+ size_t board_end = url.find('/', index);
+ if(board_end == std::string::npos)
+ return false;
+
+ board_id = url.substr(index, board_end - index);
+ index = board_end + 1;
+
+ const std::string_view remaining(url.data() + index, url.size() - index);
+ if(remaining.size() <= 7 || remaining.substr(0, 7) != "thread/")
+ return false;
+
+ index += 7;
+ size_t thread_id_end = url.find('#', index);
+ if(thread_id_end == std::string::npos)
+ thread_id_end = url.size();
+
+ thread_id = url.substr(index, thread_id_end - index);
+ if(thread_id.empty())
+ return false;
+
+ index = thread_id_end;
+ comment_id = url.substr(index);
+ return true;
+ }
+
int Program::run(int argc, char **argv) {
mgl_init();
@@ -390,14 +428,22 @@ namespace QuickMedia {
std::string youtube_url_converted = invidious_url_to_youtube_url(argv[i]);
std::string youtube_channel_id;
std::string youtube_video_id_dummy;
+ std::string fourchan_id_dummy;
- if(youtube_url_extract_channel_id(youtube_url_converted, youtube_channel_id, youtube_channel_url)) {
+ if(youtube_url_extract_channel_id(youtube_url_converted, youtube_channel_id, launch_url)) {
+ launch_url_type = LaunchUrlType::YOUTUBE_CHANNEL;
plugin_name = "youtube";
continue;
} else if(youtube_url_extract_id(youtube_url_converted, youtube_video_id_dummy)) {
- youtube_url = std::move(youtube_url_converted);
+ launch_url_type = LaunchUrlType::YOUTUBE_VIDEO;
+ launch_url = std::move(youtube_url_converted);
plugin_name = "youtube";
continue;
+ } else if(fourchan_extract_url(argv[i], fourchan_id_dummy, fourchan_id_dummy, fourchan_id_dummy)) {
+ launch_url_type = LaunchUrlType::FOURCHAN_THREAD;
+ launch_url = argv[i];
+ plugin_name = "4chan";
+ continue;
}
for(const auto &valid_plugin : valid_plugins) {
@@ -1283,20 +1329,32 @@ namespace QuickMedia {
categories_sukebei_body->set_items(body_items);
tabs.push_back(Tab{std::move(categories_sukebei_body), std::make_unique<NyaaSiCategoryPage>(this, true), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else if(strcmp(plugin_name, "4chan") == 0) {
- auto boards_page = std::make_unique<FourchanBoardsPage>(this, resources_root);
- FourchanBoardsPage *boards_page_ptr = boards_page.get();
+ if(launch_url_type == LaunchUrlType::FOURCHAN_THREAD) {
+ // TODO: Use comment id
+ std::string board_id, thread_id, comment_id;
+ fourchan_extract_url(launch_url, board_id, thread_id, comment_id);
+ auto body = create_body();
+ auto thread_page = std::make_unique<FourchanThreadPage>(this, std::move(board_id), std::move(thread_id), "");
+ page_stack.push(current_page);
+ current_page = PageType::IMAGE_BOARD_THREAD;
+ image_board_thread_page(thread_page.get(), body.get());
+ exit(0);
+ } else {
+ auto boards_page = std::make_unique<FourchanBoardsPage>(this, resources_root);
+ FourchanBoardsPage *boards_page_ptr = boards_page.get();
- auto boards_body = create_body();
- BodyItems body_items;
- boards_page->get_boards(body_items);
- boards_body->set_items(std::move(body_items));
- tabs.push_back(Tab{std::move(boards_body), std::move(boards_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ auto boards_body = create_body();
+ BodyItems body_items;
+ boards_page->get_boards(body_items);
+ boards_body->set_items(std::move(body_items));
+ tabs.push_back(Tab{std::move(boards_body), std::move(boards_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
- auto login_page = std::make_unique<FourchanLoginPage>(this, "4chan pass login", boards_page_ptr, &tabs, 1);
- FourchanLoginPage *login_page_ptr = login_page.get();
+ auto login_page = std::make_unique<FourchanLoginPage>(this, "4chan pass login", boards_page_ptr, &tabs, 1);
+ FourchanLoginPage *login_page_ptr = login_page.get();
- tabs.push_back(Tab{ create_body(), std::move(login_page), nullptr, {} });
- login_page_ptr->login_inputs = &tabs.back().login_inputs;
+ tabs.push_back(Tab{ create_body(), std::move(login_page), nullptr, {} });
+ login_page_ptr->login_inputs = &tabs.back().login_inputs;
+ }
} else if(strcmp(plugin_name, "hotexamples") == 0) {
auto body = create_body();
BodyItems body_items;
@@ -1322,11 +1380,11 @@ namespace QuickMedia {
pipe_body->set_items(std::move(body_items));
tabs.push_back(Tab{std::move(pipe_body), std::make_unique<PipePage>(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
} else if(strcmp(plugin_name, "youtube") == 0) {
- if(!youtube_channel_url.empty()) {
- YoutubeChannelPage::create_each_type(this, youtube_channel_url, "", "Channel", tabs);
- } else if(!youtube_url.empty()) {
+ if(launch_url_type == LaunchUrlType::YOUTUBE_CHANNEL) {
+ YoutubeChannelPage::create_each_type(this, std::move(launch_url), "", "Channel", tabs);
+ } else if(launch_url_type == LaunchUrlType::YOUTUBE_VIDEO) {
current_page = PageType::VIDEO_CONTENT;
- auto youtube_video_page = std::make_unique<YoutubeVideoPage>(this, youtube_url, false);
+ auto youtube_video_page = std::make_unique<YoutubeVideoPage>(this, std::move(launch_url), false);
video_content_page(nullptr, youtube_video_page.get(), "", false, nullptr, 0);
} else {
start_tab_index = 1;
@@ -4479,6 +4537,22 @@ namespace QuickMedia {
void Program::image_board_thread_page(ImageBoardThreadPage *thread_page, Body *thread_body) {
AsyncImageLoader::get_instance().update();
+ BodyItems result_items;
+ const TaskResult load_result = run_task_with_loading_screen([&]() {
+ return thread_page->lazy_fetch(result_items) == PluginResult::OK;
+ });
+
+ if(load_result == TaskResult::CANCEL) {
+ current_page = pop_page_stack();
+ return;
+ } else if(load_result == TaskResult::FALSE) {
+ show_notification("QuickMedia", "Failed to load thread", Urgency::CRITICAL);
+ current_page = pop_page_stack();
+ return;
+ }
+
+ thread_body->set_items(std::move(result_items));
+
// TODO: Instead of using stage here, use different pages for each stage
enum class NavigationStage {
VIEWING_COMMENTS,
@@ -4697,9 +4771,10 @@ namespace QuickMedia {
page_stack.push(PageType::IMAGE_BOARD_THREAD);
current_page = PageType::VIDEO_CONTENT;
watched_videos.clear();
- thread_page->set_url(selected_item->url);
+ ImageBoardVideoPage video_page(this);
+ video_page.set_url(selected_item->url);
// TODO: Use real title
- video_content_page(thread_page, thread_page, "", true, thread_body, thread_body->get_selected_item());
+ video_content_page(thread_page, &video_page, "", true, thread_body, thread_body->get_selected_item());
redraw = true;
idle_active_handler();
} else {
diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp
index 4a9d2d7..cf229d4 100644
--- a/src/plugins/Fourchan.cpp
+++ b/src/plugins/Fourchan.cpp
@@ -390,16 +390,100 @@ namespace QuickMedia {
needs_refresh = true;
}
- // TODO: Merge with lazy fetch
PluginResult FourchanThreadListPage::submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) {
+ result_tabs.push_back(Tab{create_body(), std::make_unique<FourchanThreadPage>(program, board_id, args.url, pass_id), nullptr});
+ return PluginResult::OK;
+ }
+
+ PluginResult FourchanThreadListPage::lazy_fetch(BodyItems &result_items) {
Json::Value json_root;
- DownloadResult result = download_json(json_root, fourchan_url + board_id + "/thread/" + args.url + ".json", {}, true);
+ DownloadResult result = download_json(json_root, fourchan_url + board_id + "/catalog.json?s=Index", {}, true);
+ if(result != DownloadResult::OK) return download_result_to_plugin_result(result);
+
+ if(!json_root.isArray())
+ return PluginResult::ERR;
+
+ std::unordered_map<int64_t, size_t> comment_by_postno;
+ for(const Json::Value &page_data : json_root) {
+ if(!page_data.isObject())
+ continue;
+
+ const Json::Value &threads = page_data["threads"];
+ if(!threads.isArray())
+ continue;
+
+ for(const Json::Value &thread : threads) {
+ if(!thread.isObject())
+ continue;
+
+ const Json::Value &sub = thread["sub"];
+ const char *sub_begin = "";
+ const char *sub_end = sub_begin;
+ sub.getString(&sub_begin, &sub_end);
+
+ const Json::Value &com = thread["com"];
+ const char *comment_begin = "";
+ const char *comment_end = comment_begin;
+ com.getString(&comment_begin, &comment_end);
+
+ const Json::Value &thread_num = thread["no"];
+ if(!thread_num.isNumeric())
+ continue;
+
+ std::string title_text = html_to_text(sub_begin, sub_end - sub_begin, comment_by_postno, result_items, 0);
+ if(!title_text.empty() && title_text.back() == '\n')
+ title_text.back() = ' ';
+
+ std::string comment_text = html_to_text(comment_begin, comment_end - comment_begin, comment_by_postno, result_items, 0);
+
+ auto body_item = BodyItem::create(std::move(comment_text));
+ body_item->set_title_max_lines(6);
+ body_item->set_author(std::move(title_text));
+ body_item->url = std::to_string(thread_num.asInt64());
+
+ const Json::Value &ext = thread["ext"];
+ const Json::Value &tim = thread["tim"];
+ if(tim.isNumeric() && ext.isString()) {
+ std::string ext_str = ext.asString();
+ if(ext_str == ".png" || ext_str == ".jpg" || ext_str == ".jpeg" || ext_str == ".webm" || ext_str == ".mp4" || ext_str == ".gif") {
+ } else {
+ fprintf(stderr, "TODO: Support file extension: %s\n", ext_str.c_str());
+ }
+ // "s" means small, that's the url 4chan uses for thumbnails.
+ // thumbnails always has .jpg extension even if they are gifs or webm.
+ body_item->thumbnail_url = fourchan_image_url + board_id + "/" + std::to_string(tim.asInt64()) + "s.jpg";
+
+ mgl::vec2i thumbnail_size(64, 64);
+ const Json::Value &tn_w = thread["tn_w"];
+ const Json::Value &tn_h = thread["tn_h"];
+ if(tn_w.isNumeric() && tn_h.isNumeric())
+ thumbnail_size = mgl::vec2i(tn_w.asInt() / 2, tn_h.asInt() / 2);
+ body_item->thumbnail_size = std::move(thumbnail_size);
+ }
+
+ result_items.push_back(std::move(body_item));
+ }
+ }
+
+ return PluginResult::OK;
+ }
+
+ static std::string file_get_filename(const std::string &filepath) {
+ size_t index = filepath.rfind('/');
+ if(index == std::string::npos)
+ return filepath.c_str();
+ return filepath.c_str() + index + 1;
+ }
+
+ // TODO: Merge with FourchanThreadListPage lazy fetch
+ PluginResult FourchanThreadPage::lazy_fetch(BodyItems &result_items) {
+ Json::Value json_root;
+ DownloadResult result = download_json(json_root, fourchan_url + board_id + "/thread/" + thread_id + ".json", {}, true);
if(result != DownloadResult::OK) return download_result_to_plugin_result(result);
if(!json_root.isObject())
return PluginResult::ERR;
- BodyItems result_items;
std::unordered_map<int64_t, size_t> comment_by_postno;
const Json::Value &posts = json_root["posts"];
@@ -484,92 +568,9 @@ namespace QuickMedia {
++body_item_index;
}
- auto body = create_body(false);
- body->set_items(std::move(result_items));
- result_tabs.push_back(Tab{std::move(body), std::make_unique<FourchanThreadPage>(program, board_id, args.url, pass_id), nullptr});
return PluginResult::OK;
}
- PluginResult FourchanThreadListPage::lazy_fetch(BodyItems &result_items) {
- Json::Value json_root;
- DownloadResult result = download_json(json_root, fourchan_url + board_id + "/catalog.json?s=Index", {}, true);
- if(result != DownloadResult::OK) return download_result_to_plugin_result(result);
-
- if(!json_root.isArray())
- return PluginResult::ERR;
-
- std::unordered_map<int64_t, size_t> comment_by_postno;
- for(const Json::Value &page_data : json_root) {
- if(!page_data.isObject())
- continue;
-
- const Json::Value &threads = page_data["threads"];
- if(!threads.isArray())
- continue;
-
- for(const Json::Value &thread : threads) {
- if(!thread.isObject())
- continue;
-
- const Json::Value &sub = thread["sub"];
- const char *sub_begin = "";
- const char *sub_end = sub_begin;
- sub.getString(&sub_begin, &sub_end);
-
- const Json::Value &com = thread["com"];
- const char *comment_begin = "";
- const char *comment_end = comment_begin;
- com.getString(&comment_begin, &comment_end);
-
- const Json::Value &thread_num = thread["no"];
- if(!thread_num.isNumeric())
- continue;
-
- std::string title_text = html_to_text(sub_begin, sub_end - sub_begin, comment_by_postno, result_items, 0);
- if(!title_text.empty() && title_text.back() == '\n')
- title_text.back() = ' ';
-
- std::string comment_text = html_to_text(comment_begin, comment_end - comment_begin, comment_by_postno, result_items, 0);
-
- auto body_item = BodyItem::create(std::move(comment_text));
- body_item->set_title_max_lines(6);
- body_item->set_author(std::move(title_text));
- body_item->url = std::to_string(thread_num.asInt64());
-
- const Json::Value &ext = thread["ext"];
- const Json::Value &tim = thread["tim"];
- if(tim.isNumeric() && ext.isString()) {
- std::string ext_str = ext.asString();
- if(ext_str == ".png" || ext_str == ".jpg" || ext_str == ".jpeg" || ext_str == ".webm" || ext_str == ".mp4" || ext_str == ".gif") {
- } else {
- fprintf(stderr, "TODO: Support file extension: %s\n", ext_str.c_str());
- }
- // "s" means small, that's the url 4chan uses for thumbnails.
- // thumbnails always has .jpg extension even if they are gifs or webm.
- body_item->thumbnail_url = fourchan_image_url + board_id + "/" + std::to_string(tim.asInt64()) + "s.jpg";
-
- mgl::vec2i thumbnail_size(64, 64);
- const Json::Value &tn_w = thread["tn_w"];
- const Json::Value &tn_h = thread["tn_h"];
- if(tn_w.isNumeric() && tn_h.isNumeric())
- thumbnail_size = mgl::vec2i(tn_w.asInt() / 2, tn_h.asInt() / 2);
- body_item->thumbnail_size = std::move(thumbnail_size);
- }
-
- result_items.push_back(std::move(body_item));
- }
- }
-
- return PluginResult::OK;
- }
-
- static std::string file_get_filename(const std::string &filepath) {
- size_t index = filepath.rfind('/');
- if(index == std::string::npos)
- return filepath.c_str();
- return filepath.c_str() + index + 1;
- }
-
PostResult FourchanThreadPage::post_comment(const std::string &captcha_id, const std::string &captcha_solution, const std::string &comment, const std::string &filepath) {
Path cookies_filepath;
if(get_cookies_filepath(cookies_filepath, SERVICE_NAME) != 0) {