From 89c41c1488854858e02ff6bd48a6518161fa05a5 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Tue, 22 Nov 2022 01:44:39 +0100 Subject: Allow launching directly into 4chan thread --- README.md | 4 +- include/QuickMedia.hpp | 11 ++- plugins/Fourchan.hpp | 2 + plugins/ImageBoard.hpp | 12 +++- src/QuickMedia.cpp | 117 ++++++++++++++++++++++++++------ src/plugins/Fourchan.cpp | 173 ++++++++++++++++++++++++----------------------- 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 ] [--instance ] [-e ] [--video-max-height ] [youtube-url] [youtube-channel-url] +usage: quickmedia [plugin|] [--no-video] [--upscale-images] [--upscale-images-always] [--dir ] [--instance ] [-e ] [--video-max-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 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 ] [--instance ] [-e ] [--video-max-height ] [youtube-url] [youtube-channel-url]\n"); + fprintf(stderr, "usage: quickmedia [plugin|url] [--no-video] [--upscale-images] [--upscale-images-always] [--dir ] [--instance ] [-e ] [--video-max-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(this, true), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } else if(strcmp(plugin_name, "4chan") == 0) { - auto boards_page = std::make_unique(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(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(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(this, "4chan pass login", boards_page_ptr, &tabs, 1); - FourchanLoginPage *login_page_ptr = login_page.get(); + auto login_page = std::make_unique(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(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(this, youtube_url, false); + auto youtube_video_page = std::make_unique(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 &result_tabs) { + result_tabs.push_back(Tab{create_body(), std::make_unique(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 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 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(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 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) { -- cgit v1.2.3