From 77ed51898157d99112be7550471ec06e32344c9e Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 11 Oct 2020 21:35:37 +0200 Subject: Refactor plugin into seperate pages TODO: Readd 4chan login page, manganelo creators page, autocomplete --- src/Body.cpp | 18 +- src/DownloadUtils.cpp | 2 + src/ImageViewer.cpp | 4 +- src/QuickMedia.cpp | 2251 ++++++++++++++++--------------------------- src/SearchBar.cpp | 29 +- src/Storage.cpp | 21 + src/plugins/Dmenu.cpp | 23 - src/plugins/FileManager.cpp | 75 +- src/plugins/Fourchan.cpp | 649 ++++++------- src/plugins/ImageBoard.cpp | 30 + src/plugins/Manga.cpp | 9 +- src/plugins/Mangadex.cpp | 201 ++-- src/plugins/Manganelo.cpp | 277 +++--- src/plugins/Mangatown.cpp | 212 ++-- src/plugins/Matrix.cpp | 41 +- src/plugins/NyaaSi.cpp | 131 +-- src/plugins/Page.cpp | 50 + src/plugins/Plugin.cpp | 52 +- src/plugins/Pornhub.cpp | 23 +- src/plugins/Youtube.cpp | 317 ++---- 20 files changed, 1853 insertions(+), 2562 deletions(-) delete mode 100644 src/plugins/Dmenu.cpp create mode 100644 src/plugins/ImageBoard.cpp create mode 100644 src/plugins/Page.cpp (limited to 'src') diff --git a/src/Body.cpp b/src/Body.cpp index 260e90d..ac63e0f 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -98,11 +98,6 @@ namespace QuickMedia { item_background.setFillColor(sf::Color(55, 60, 68)); } - Body::~Body() { - if(load_thumbnail_future.valid()) - load_thumbnail_future.get(); - } - // TODO: Make this work with wraparound enabled? // TODO: For plugins with different sized body items this can be weird, because after scrolling down thumbnails could load and they could move items up/down until we see items we haven't seen bool Body::select_previous_page() { @@ -250,6 +245,17 @@ namespace QuickMedia { } } + void Body::clear_cache() { + clear_text_cache(); + clear_thumbnails(); + } + + void Body::clear_text_cache() { + for(auto &body_item : items) { + clear_body_item_cache(body_item.get()); + } + } + void Body::clear_thumbnails() { item_thumbnail_textures.clear(); } @@ -566,7 +572,7 @@ namespace QuickMedia { if(draw_thumbnails) { if(!item->thumbnail_url.empty() && item_thumbnail->loading_state == LoadingState::NOT_LOADED) { - async_image_loader.load_thumbnail(item->thumbnail_url, item->thumbnail_is_local, thumbnail_resize_target_size, program->get_current_plugin()->use_tor, item_thumbnail); + async_image_loader.load_thumbnail(item->thumbnail_url, item->thumbnail_is_local, thumbnail_resize_target_size, program->is_tor_enabled(), item_thumbnail); } if(item_thumbnail->loading_state == LoadingState::FINISHED_LOADING && item_thumbnail->image->getSize().x > 0 && item_thumbnail->image->getSize().y > 0) { diff --git a/src/DownloadUtils.cpp b/src/DownloadUtils.cpp index dc99028..c44bed5 100644 --- a/src/DownloadUtils.cpp +++ b/src/DownloadUtils.cpp @@ -8,6 +8,8 @@ static const bool debug_download = false; static int accumulate_string(char *data, int size, void *userdata) { std::string *str = (std::string*)userdata; + if(str->size() + size > 1024 * 1024 * 100) // 100mb sane limit, TODO: make configurable + return 1; str->append(data, size); return 0; } diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp index 77c53b4..b544165 100644 --- a/src/ImageViewer.cpp +++ b/src/ImageViewer.cpp @@ -8,7 +8,7 @@ #include namespace QuickMedia { - ImageViewer::ImageViewer(Manga *manga, const std::string &images_url, const std::string &content_title, const std::string &chapter_title, int current_page, const Path &chapter_cache_dir, sf::Font *font) : + ImageViewer::ImageViewer(MangaImagesPage *manga_images_page, const std::string &content_title, const std::string &chapter_title, int current_page, const Path &chapter_cache_dir, sf::Font *font) : current_page(current_page), num_pages(0), content_title(content_title), @@ -18,7 +18,7 @@ namespace QuickMedia { font(font), page_text("", *font, 14) { - if(manga->get_number_of_images(images_url, num_pages) != ImageResult::OK) { + if(manga_images_page->get_number_of_images(num_pages) != ImageResult::OK) { show_notification("Plugin", "Failed to get number of images", Urgency::CRITICAL); return; } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 3922ceb..01e244a 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -5,7 +5,6 @@ #include "../plugins/Youtube.hpp" #include "../plugins/Pornhub.hpp" #include "../plugins/Fourchan.hpp" -#include "../plugins/Dmenu.hpp" #include "../plugins/NyaaSi.hpp" #include "../plugins/Matrix.hpp" #include "../plugins/FileManager.hpp" @@ -166,13 +165,102 @@ static sf::Color interpolate_colors(sf::Color source, sf::Color target, double p } namespace QuickMedia { + class HistoryPage : public Page { + public: + HistoryPage(Program *program, Page *search_page) : Page(program), search_page(search_page) {} + const char* get_title() const override { return "History"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override { + return search_page->submit(title, url, result_tabs); + } + private: + Page *search_page; + }; + + class RecommendedPage : public Page { + public: + RecommendedPage(Program *program, Page *search_page) : Page(program), search_page(search_page) {} + const char* get_title() const override { return "Recommended"; } + PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override { + return search_page->submit(title, url, result_tabs); + } + private: + Page *search_page; + }; + + // TODO: Make asynchronous + static void fill_recommended_items_from_json(const Json::Value &recommended_json, BodyItems &body_items) { + assert(recommended_json.isObject()); + + std::vector> recommended_items(recommended_json.size()); + /* TODO: Optimize member access */ + for(auto &member_name : recommended_json.getMemberNames()) { + Json::Value recommended_item = recommended_json[member_name]; + if(recommended_item.isObject()) + recommended_items.push_back(std::make_pair(member_name, std::move(recommended_item))); + } + + /* TODO: Better algorithm for recommendations */ + std::sort(recommended_items.begin(), recommended_items.end(), [](std::pair &a, std::pair &b) { + Json::Value &a_timestamp_json = a.second["recommended_timestamp"]; + Json::Value &b_timestamp_json = b.second["recommended_timestamp"]; + int64_t a_timestamp = 0; + int64_t b_timestamp = 0; + if(a_timestamp_json.isNumeric()) + a_timestamp = a_timestamp_json.asInt64(); + if(b_timestamp_json.isNumeric()) + b_timestamp = b_timestamp_json.asInt64(); + + Json::Value &a_recommended_count_json = a.second["recommended_count"]; + Json::Value &b_recommended_count_json = b.second["recommended_count"]; + int64_t a_recommended_count = 0; + int64_t b_recommended_count = 0; + if(a_recommended_count_json.isNumeric()) + a_recommended_count = a_recommended_count_json.asInt64(); + if(b_recommended_count_json.isNumeric()) + b_recommended_count = b_recommended_count_json.asInt64(); + + /* Put frequently recommended videos on top of recommendations. Each recommendation count is worth 5 minutes */ + a_timestamp += (300 * a_recommended_count); + b_timestamp += (300 * b_recommended_count); + + return a_timestamp > b_timestamp; + }); + + for(auto it = recommended_items.begin(); it != recommended_items.end(); ++it) { + const std::string &recommended_item_id = it->first; + Json::Value &recommended_item = it->second; + + int64_t watched_count = 0; + const Json::Value &watched_count_json = recommended_item["watched_count"]; + if(watched_count_json.isNumeric()) + watched_count = watched_count_json.asInt64(); + + /* TODO: Improve recommendations with some kind of algorithm. Videos we have seen should be recommended in some cases */ + if(watched_count != 0) + continue; + + const Json::Value &recommended_title_json = recommended_item["title"]; + if(!recommended_title_json.isString()) + continue; + + auto body_item = BodyItem::create(recommended_title_json.asString()); + body_item->url = "https://www.youtube.com/watch?v=" + recommended_item_id; + body_item->thumbnail_url = "https://img.youtube.com/vi/" + recommended_item_id + "/hqdefault.jpg"; + body_items.push_back(std::move(body_item)); + + // We dont want more than 150 recommendations + if(body_items.size() == 150) + break; + } + + std::random_shuffle(body_items.begin(), body_items.end()); + } + Program::Program() : disp(nullptr), window(sf::VideoMode(1280, 720), "QuickMedia", sf::Style::Default, sf::ContextSettings(0, 0, 0, 3, 3)), window_size(1280, 720), - body(nullptr), - current_plugin(nullptr), - current_page(Page::SEARCH_SUGGESTION), + current_page(PageType::EXIT), image_index(0) { disp = XOpenDisplay(NULL); @@ -223,10 +311,6 @@ namespace QuickMedia { abort(); } - body = new Body(this, font.get(), bold_font.get(), cjk_font.get()); - related_media_body = new Body(this, font.get(), bold_font.get(), cjk_font.get()); - related_media_body->draw_thumbnails = true; - struct sigaction action; action.sa_handler = sigpipe_handler; sigemptyset(&action.sa_mask); @@ -262,69 +346,29 @@ namespace QuickMedia { } else { running = false; } - if(related_media_body) - delete related_media_body; - if(body) - delete body; - if(file_manager) - delete file_manager; - if(current_plugin && current_plugin != file_manager) - delete current_plugin; + if(matrix) + delete matrix; if(disp) XCloseDisplay(disp); } - static SearchResult search_selected_suggestion(Body *input_body, Body *output_body, Plugin *plugin, std::string &selected_title, std::string &selected_url, bool skip_search) { - BodyItem *selected_item = input_body->get_selected(); - if(!selected_item) - return SearchResult::ERR; - - selected_title = selected_item->get_title(); - selected_url = selected_item->url; - if(!skip_search) { - output_body->clear_items(); - SearchResult search_result = plugin->search(!selected_url.empty() ? selected_url : selected_title, output_body->items); - output_body->reset_selected(); - return search_result; - } else { - return SearchResult::OK; - } - } - static void usage() { fprintf(stderr, "usage: QuickMedia [--tor] [--no-video] [--use-system-mpv-config] [--dir ] [-p ]\n"); fprintf(stderr, "OPTIONS:\n"); - fprintf(stderr, " plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, pornhub, youtube, nyaa.si, matrix, file-manager or dmenu\n"); + fprintf(stderr, " plugin The plugin to use. Should be either 4chan, manganelo, mangatown, mangadex, pornhub, youtube, nyaa.si, matrix, file-manager\n"); fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n"); fprintf(stderr, " --tor Use tor. 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"); fprintf(stderr, " --upscale-images-force Upscale manga pages using waifu2x-ncnn-vulkan, no matter what the original image resolution is. Disabled by default\n"); fprintf(stderr, " --dir Set the start directory when using file-manager\n"); - fprintf(stderr, " -p Change the placeholder text for dmenu\n"); fprintf(stderr, "EXAMPLES:\n"); fprintf(stderr, "QuickMedia manganelo\n"); fprintf(stderr, "QuickMedia youtube --tor\n"); - fprintf(stderr, "echo \"hello\\nworld\" | QuickMedia dmenu\n"); } - static bool is_program_executable_by_name(const char *name) { - // TODO: Implement for Windows. Windows also uses semicolon instead of colon as a separator - char *env = getenv("PATH"); - std::unordered_set paths; - string_split(env, ':', [&paths](const char *str, size_t size) { - paths.insert(std::string(str, size)); - return true; - }); - - for(const std::string &path_str : paths) { - Path path(path_str); - path.join(name); - if(get_file_type(path) == FileType::REGULAR) - return true; - } - - return false; + static bool is_manga_plugin(const char *plugin_name) { + return strcmp(plugin_name, "manganelo") == 0 || strcmp(plugin_name, "mangatown") == 0 || strcmp(plugin_name, "mangadex") == 0; } int Program::run(int argc, char **argv) { @@ -333,45 +377,40 @@ namespace QuickMedia { return -1; } - current_plugin = nullptr; std::string plugin_logo_path; - std::string search_placeholder; const char *start_dir = nullptr; + std::vector tabs; for(int i = 1; i < argc; ++i) { - if(!current_plugin) { + if(!plugin_name) { if(strcmp(argv[i], "manganelo") == 0) { - current_plugin = new Manganelo(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/manganelo_logo.png"; } else if(strcmp(argv[i], "mangatown") == 0) { - current_plugin = new Mangatown(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/mangatown_logo.png"; } else if(strcmp(argv[i], "mangadex") == 0) { - current_plugin = new Mangadex(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/mangadex_logo.png"; } else if(strcmp(argv[i], "youtube") == 0) { - current_plugin = new Youtube(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/yt_logo_rgb_dark_small.png"; } else if(strcmp(argv[i], "pornhub") == 0) { - current_plugin = new Pornhub(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/pornhub_logo.png"; + plugin_name = argv[i]; } else if(strcmp(argv[i], "4chan") == 0) { - current_plugin = new Fourchan(resources_root); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/4chan_logo.png"; } else if(strcmp(argv[i], "nyaa.si") == 0) { - current_plugin = new NyaaSi(); + plugin_name = argv[i]; plugin_logo_path = resources_root + "images/nyaa_si_logo.png"; } else if(strcmp(argv[i], "matrix") == 0) { - current_plugin = new Matrix(); + plugin_name = argv[i]; + matrix = new Matrix(); plugin_logo_path = resources_root + "images/matrix_logo.png"; } else if(strcmp(argv[i], "file-manager") == 0) { - current_plugin = new FileManager(); - } else if(strcmp(argv[i], "dmenu") == 0) { - current_plugin = new Dmenu(); - } else { - fprintf(stderr, "Invalid plugin %s\n", argv[i]); - usage(); - return -1; + plugin_name = argv[i]; } } @@ -390,11 +429,6 @@ namespace QuickMedia { start_dir = argv[i + 1]; ++i; } - } else if(strcmp(argv[i], "-p") == 0) { - if(i < argc - 1) { - search_placeholder = argv[i + 1]; - ++i; - } } else if(argv[i][0] == '-') { fprintf(stderr, "Invalid option %s\n", argv[i]); usage(); @@ -402,43 +436,19 @@ namespace QuickMedia { } } - if(!current_plugin) { + if(!plugin_name) { fprintf(stderr, "Missing plugin argument\n"); usage(); return -1; } - if(!search_placeholder.empty() && current_plugin->name == "dmenu") { - fprintf(stderr, "Option -p is only valid with dmenu\n"); - usage(); - return -1; - } - - if(current_plugin->name == "file-manager") { - current_page = Page::FILE_MANAGER; - file_manager = static_cast(current_plugin); - } else { - if(start_dir) { - fprintf(stderr, "Option --dir is only valid with file-manager\n"); - usage(); - return -1; - } - } - - if(start_dir) { - if(!static_cast(current_plugin)->set_current_directory(start_dir)) { - fprintf(stderr, "Invalid directory provided with --dir: %s\n", start_dir); - return -3; - } - } - if(use_tor && !is_program_executable_by_name("torsocks")) { fprintf(stderr, "torsocks needs to be installed (and accessible from PATH environment variable) when using the --tor option\n"); return -2; } if(upscale_image_action != UpscaleImageAction::NO) { - if(!current_plugin->is_manga()) { + if(!is_manga_plugin(plugin_name)) { fprintf(stderr, "Option --upscale-images/-upscale-images-force is only valid for manganelo, mangatown and mangadex\n"); return -2; } @@ -485,8 +495,13 @@ namespace QuickMedia { running = true; } - current_plugin->use_tor = use_tor; - window.setTitle("QuickMedia - " + current_plugin->name); + if(strcmp(plugin_name, "file-manager") != 0 && start_dir) { + fprintf(stderr, "Option --dir is only valid with file-manager\n"); + usage(); + return -1; + } + + window.setTitle("QuickMedia - " + std::string(plugin_name)); if(!plugin_logo_path.empty()) { if(!plugin_logo.loadFromFile(plugin_logo_path)) { @@ -497,99 +512,94 @@ namespace QuickMedia { plugin_logo.setSmooth(true); } - if(current_plugin->name == "matrix") { - Matrix *matrix = static_cast(current_plugin); - if(matrix->load_and_verify_cached_session() == PluginResult::OK) { - current_page = Page::CHAT; - } else { - fprintf(stderr, "Failed to load session cache, redirecting to login page\n"); - current_page = Page::CHAT_LOGIN; + if(strcmp(plugin_name, "manganelo") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique(this), create_search_bar("Search...", 200)}); + + auto history_body = create_body(); + manga_get_watch_history(plugin_name, history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "mangatown") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique(this), create_search_bar("Search...", 200)}); + + auto history_body = create_body(); + manga_get_watch_history(plugin_name, history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "mangadex") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique(this), create_search_bar("Search...", 300)}); + + auto history_body = create_body(); + manga_get_watch_history(plugin_name, history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "nyaa.si") == 0) { + auto category_page = std::make_unique(this); + auto categories_body = create_body(); + category_page->get_categories(categories_body->items); + tabs.push_back(Tab{std::move(categories_body), std::move(category_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "4chan") == 0) { + auto boards_page = std::make_unique(this, resources_root); + 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, "file-manager") == 0) { + auto file_manager_page = std::make_unique(this); + if(start_dir && !file_manager_page->set_current_directory(start_dir)) { + fprintf(stderr, "Invalid directory provided with --dir: %s\n", start_dir); + return -3; } + auto file_manager_body = create_body(); + file_manager_page->get_files_in_directory(file_manager_body->items); + tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "youtube") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique(this), create_search_bar("Search...", 350)}); + + auto history_body = create_body(); + history_body->draw_thumbnails = true; + youtube_get_watch_history(history_body->items); + tabs.push_back(Tab{std::move(history_body), std::make_unique(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + auto recommended_body = create_body(); + recommended_body->draw_thumbnails = true; + fill_recommended_items_from_json(load_recommended_json(), recommended_body->items); + tabs.push_back(Tab{std::move(recommended_body), std::make_unique(this, tabs.front().page.get()), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "pornhub") == 0) { + auto search_body = create_body(); + search_body->draw_thumbnails = true; + tabs.push_back(Tab{std::move(search_body), std::make_unique(this), create_search_bar("Search...", 500)}); } - if(search_placeholder.empty()) - search_placeholder = "Search..."; + if(!tabs.empty()) { + page_loop(std::move(tabs)); + return exit_code; + } - search_bar = std::make_unique(*font, &plugin_logo, search_placeholder); - search_bar->text_autosearch_delay = current_plugin->get_search_delay(); + if(matrix) { + matrix->use_tor = use_tor; + if(matrix->load_and_verify_cached_session() == PluginResult::OK) { + current_page = PageType::CHAT; + } else { + fprintf(stderr, "Failed to load session cache, redirecting to login page\n"); + current_page = PageType::CHAT_LOGIN; + } - while(window.isOpen()) { - switch(current_page) { - case Page::EXIT: - window.close(); - break; - case Page::SEARCH_SUGGESTION: - body->draw_thumbnails = current_plugin->search_suggestions_has_thumbnails(); - search_suggestion_page(); - body->clear_thumbnails(); - break; - case Page::VIDEO_CONTENT: - body->draw_thumbnails = false; - video_content_page(); - break; - case Page::EPISODE_LIST: - body->draw_thumbnails = false; - episode_list_page(); - body->clear_thumbnails(); - break; - case Page::IMAGES: { - body->draw_thumbnails = false; - window.setKeyRepeatEnabled(false); - window.setFramerateLimit(20); - image_page(); - body->filter_search_fuzzy(""); - if(vsync_set) - window.setFramerateLimit(0); - else - window.setFramerateLimit(monitor_hz); - window.setKeyRepeatEnabled(true); - break; - } - case Page::IMAGES_CONTINUOUS: { - body->draw_thumbnails = false; - window.setKeyRepeatEnabled(false); - image_continuous_page(); - body->filter_search_fuzzy(""); - window.setKeyRepeatEnabled(true); - break; - } - case Page::CONTENT_LIST: { - body->draw_thumbnails = true; - content_list_page(); - body->clear_thumbnails(); - break; - } - case Page::CONTENT_DETAILS: { - body->draw_thumbnails = true; - content_details_page(); - body->clear_thumbnails(); - break; - } - case Page::IMAGE_BOARD_THREAD_LIST: { - body->draw_thumbnails = true; - image_board_thread_list_page(); - body->clear_thumbnails(); - break; - } - case Page::IMAGE_BOARD_THREAD: { - body->draw_thumbnails = true; - image_board_thread_page(); - body->clear_thumbnails(); - break; - } - case Page::CHAT_LOGIN: { - chat_login_page(); - break; - } - case Page::CHAT: { - body->draw_thumbnails = true; - chat_page(); - break; - } - case Page::FILE_MANAGER: { - body->draw_thumbnails = true; - file_manager_page(); - break; + while(window.isOpen()) { + switch(current_page) { + case PageType::CHAT_LOGIN: + chat_login_page(); + break; + case PageType::CHAT: + chat_page(); + break; + default: + window.close(); + break; } } } @@ -597,9 +607,10 @@ namespace QuickMedia { return exit_code; } - void Program::base_event_handler(sf::Event &event, Page previous_page, bool handle_keypress, bool clear_on_escape, bool handle_searchbar) { + void Program::base_event_handler(sf::Event &event, PageType previous_page, Body *body, SearchBar *search_bar, bool handle_keypress, bool handle_searchbar) { if (event.type == sf::Event::Closed) { - current_page = Page::EXIT; + current_page = PageType::EXIT; + window.close(); } else if(event.type == sf::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; @@ -620,13 +631,9 @@ namespace QuickMedia { body->select_last_item(); } else if(event.key.code == sf::Keyboard::Escape) { current_page = previous_page; - if(clear_on_escape) { - body->clear_items(); - body->reset_selected(); - search_bar->clear(); - } } } else if(handle_searchbar) { + assert(search_bar); if(event.type == sf::Event::TextEntered) search_bar->onTextEntered(event.text.unicode); search_bar->on_event(event); @@ -707,76 +714,7 @@ namespace QuickMedia { } } - // TODO: Make asynchronous - static void fill_recommended_items_from_json(const Json::Value &recommended_json, BodyItems &body_items) { - assert(recommended_json.isObject()); - - std::vector> recommended_items(recommended_json.size()); - /* TODO: Optimize member access */ - for(auto &member_name : recommended_json.getMemberNames()) { - Json::Value recommended_item = recommended_json[member_name]; - if(recommended_item.isObject()) - recommended_items.push_back(std::make_pair(member_name, std::move(recommended_item))); - } - - /* TODO: Better algorithm for recommendations */ - std::sort(recommended_items.begin(), recommended_items.end(), [](std::pair &a, std::pair &b) { - Json::Value &a_timestamp_json = a.second["recommended_timestamp"]; - Json::Value &b_timestamp_json = b.second["recommended_timestamp"]; - int64_t a_timestamp = 0; - int64_t b_timestamp = 0; - if(a_timestamp_json.isNumeric()) - a_timestamp = a_timestamp_json.asInt64(); - if(b_timestamp_json.isNumeric()) - b_timestamp = b_timestamp_json.asInt64(); - - Json::Value &a_recommended_count_json = a.second["recommended_count"]; - Json::Value &b_recommended_count_json = b.second["recommended_count"]; - int64_t a_recommended_count = 0; - int64_t b_recommended_count = 0; - if(a_recommended_count_json.isNumeric()) - a_recommended_count = a_recommended_count_json.asInt64(); - if(b_recommended_count_json.isNumeric()) - b_recommended_count = b_recommended_count_json.asInt64(); - - /* Put frequently recommended videos on top of recommendations. Each recommendation count is worth 5 minutes */ - a_timestamp += (300 * a_recommended_count); - b_timestamp += (300 * b_recommended_count); - - return a_timestamp > b_timestamp; - }); - - for(auto it = recommended_items.begin(); it != recommended_items.end(); ++it) { - const std::string &recommended_item_id = it->first; - Json::Value &recommended_item = it->second; - - int64_t watched_count = 0; - const Json::Value &watched_count_json = recommended_item["watched_count"]; - if(watched_count_json.isNumeric()) - watched_count = watched_count_json.asInt64(); - - /* TODO: Improve recommendations with some kind of algorithm. Videos we have seen should be recommended in some cases */ - if(watched_count != 0) - continue; - - const Json::Value &recommended_title_json = recommended_item["title"]; - if(!recommended_title_json.isString()) - continue; - - auto body_item = BodyItem::create(recommended_title_json.asString()); - body_item->url = "https://www.youtube.com/watch?v=" + recommended_item_id; - body_item->thumbnail_url = "https://img.youtube.com/vi/" + recommended_item_id + "/hqdefault.jpg"; - body_items.push_back(std::move(body_item)); - - // We dont want more than 150 recommendations - if(body_items.size() == 150) - break; - } - - std::random_shuffle(body_items.begin(), body_items.end()); - } - - static Path get_video_history_filepath(Plugin *plugin) { + static Path get_video_history_filepath(const char *plugin_name) { Path video_history_dir = get_storage_dir().join("history"); if(create_directory_recursive(video_history_dir) != 0) { std::string err_msg = "Failed to create video history directory "; @@ -786,10 +724,10 @@ namespace QuickMedia { } Path video_history_filepath = video_history_dir; - return video_history_filepath.join(plugin->name).append(".json"); + return video_history_filepath.join(plugin_name).append(".json"); } - static Path get_recommended_filepath(Plugin *plugin) { + static Path get_recommended_filepath(const char *plugin_name) { Path video_history_dir = get_storage_dir().join("recommended"); if(create_directory_recursive(video_history_dir) != 0) { std::string err_msg = "Failed to create recommended directory "; @@ -799,13 +737,13 @@ namespace QuickMedia { } Path video_history_filepath = video_history_dir; - return video_history_filepath.join(plugin->name).append(".json"); + return video_history_filepath.join(plugin_name).append(".json"); } // This is not cached because we could have multiple instances of QuickMedia running the same plugin! // TODO: Find a way to optimize this - Json::Value Program::load_video_history_json(Plugin *plugin) { - Path video_history_filepath = get_video_history_filepath(plugin); + Json::Value Program::load_video_history_json() { + Path video_history_filepath = get_video_history_filepath(plugin_name); Json::Value json_result; if(!read_file_as_json(video_history_filepath, json_result) || !json_result.isArray()) json_result = Json::Value(Json::arrayValue); @@ -814,66 +752,62 @@ namespace QuickMedia { // This is not cached because we could have multiple instances of QuickMedia running the same plugin! // TODO: Find a way to optimize this - Json::Value Program::load_recommended_json(Plugin *plugin) { - Path recommended_filepath = get_recommended_filepath(plugin); + Json::Value Program::load_recommended_json() { + Path recommended_filepath = get_recommended_filepath(plugin_name); Json::Value json_result; if(!read_file_as_json(recommended_filepath, json_result) || !json_result.isObject()) json_result = Json::Value(Json::objectValue); return json_result; } - void Program::plugin_get_watch_history(Plugin *plugin, BodyItems &history_items) { + void Program::manga_get_watch_history(const char *plugin_name, BodyItems &history_items) { // TOOD: Make generic, instead of checking for plugin - if(plugin->is_manga()) { - Path content_storage_dir = get_storage_dir().join(plugin->name); - if(create_directory_recursive(content_storage_dir) != 0) { - show_notification("Storage", "Failed to create directory: " + content_storage_dir.data, Urgency::CRITICAL); - exit(1); - } - Path credentials_storage_dir = get_storage_dir().join("credentials"); - if(create_directory_recursive(credentials_storage_dir) != 0) { - show_notification("Storage", "Failed to create directory: " + credentials_storage_dir.data, Urgency::CRITICAL); - exit(1); - } - // TODO: Make asynchronous - for_files_in_dir_sort_last_modified(content_storage_dir, [&history_items, plugin](const std::filesystem::path &filepath) { - // This can happen when QuickMedia crashes/is killed while writing to storage. - // In that case, the storage wont be corrupt but there will be .tmp files. - // TODO: Remove these .tmp files if they exist during startup - if(filepath.extension() == ".tmp") - return true; - - Path fullpath(filepath.c_str()); - Json::Value body; - if(!read_file_as_json(fullpath, body)) { - fprintf(stderr, "Failed to read json file: %s\n", fullpath.data.c_str()); - return true; - } + Path content_storage_dir = get_storage_dir().join(plugin_name); + if(create_directory_recursive(content_storage_dir) != 0) { + show_notification("Storage", "Failed to create directory: " + content_storage_dir.data, Urgency::CRITICAL); + exit(1); + } + Path credentials_storage_dir = get_storage_dir().join("credentials"); + if(create_directory_recursive(credentials_storage_dir) != 0) { + show_notification("Storage", "Failed to create directory: " + credentials_storage_dir.data, Urgency::CRITICAL); + exit(1); + } + // TODO: Make asynchronous + for_files_in_dir_sort_last_modified(content_storage_dir, [&history_items, plugin_name](const std::filesystem::path &filepath) { + // This can happen when QuickMedia crashes/is killed while writing to storage. + // In that case, the storage wont be corrupt but there will be .tmp files. + // TODO: Remove these .tmp files if they exist during startup + if(filepath.extension() == ".tmp") + return true; - auto filename = filepath.filename(); - const Json::Value &manga_name = body["name"]; - if(!filename.empty() && manga_name.isString()) { - // TODO: Add thumbnail - auto body_item = BodyItem::create(manga_name.asString()); - if(plugin->name == "manganelo") - body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string()); - else if(plugin->name == "mangadex") - body_item->url = "https://mangadex.org/title/" + base64_decode(filename.string()); - else if(plugin->name == "mangatown") - body_item->url = "https://mangatown.com/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)); - } + Path fullpath(filepath.c_str()); + Json::Value body; + if(!read_file_as_json(fullpath, body) || !body.isObject()) { + fprintf(stderr, "Failed to read json file: %s\n", fullpath.data.c_str()); return true; - }); - return; - } + } - if(plugin->name != "youtube") - return; + auto filename = filepath.filename(); + const Json::Value &manga_name = body["name"]; + if(!filename.empty() && manga_name.isString()) { + // TODO: Add thumbnail + auto body_item = BodyItem::create(manga_name.asString()); + if(strcmp(plugin_name, "manganelo") == 0) + body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string()); + else if(strcmp(plugin_name, "mangadex") == 0) + body_item->url = "https://mangadex.org/title/" + base64_decode(filename.string()); + else if(strcmp(plugin_name, "mangatown") == 0) + body_item->url = "https://mangatown.com/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)); + } + return true; + }); + } - fill_history_items_from_json(load_video_history_json(plugin), history_items); + void Program::youtube_get_watch_history(BodyItems &history_items) { + fill_history_items_from_json(load_video_history_json(), history_items); } static void get_body_dimensions(const sf::Vector2f &window_size, SearchBar *search_bar, sf::Vector2f &body_pos, sf::Vector2f &body_size, bool has_tabs = false) { @@ -890,7 +824,7 @@ namespace QuickMedia { if(!has_tabs) tab_h = 0.0f; - float search_bottom = search_bar->getBottomWithoutShadow(); + float search_bottom = search_bar ? search_bar->getBottomWithoutShadow() : 0.0f; body_pos = sf::Vector2f(body_padding_horizontal, search_bottom + body_padding_vertical + tab_h); body_size = sf::Vector2f(body_width, window_size.y - search_bottom - body_padding_vertical - tab_h); } @@ -922,173 +856,189 @@ namespace QuickMedia { std::unique_ptr password; }; - struct Tab { - Body *body; - std::unique_ptr login_tab; - SearchSuggestionTab tab; - sf::Text *text; - }; - - bool Program::on_search_suggestion_submit_text(Body *input_body, Body *output_body) { - if(input_body->no_items_visible()) - return false; - - Page next_page = current_plugin->get_page_after_search(); - bool skip_search = (next_page == Page::VIDEO_CONTENT || next_page == Page::CONTENT_LIST); - // TODO: This shouldn't be done if search_selected_suggestion fails - if(search_selected_suggestion(input_body, output_body, current_plugin, content_title, content_url, skip_search) != SearchResult::OK) { - show_notification("Search", "Search failed!", Urgency::CRITICAL); - return false; - } + bool Program::is_tor_enabled() { + return use_tor; + } - if(next_page == Page::EPISODE_LIST && current_plugin->is_manga()) { - Manga *manga_plugin = static_cast(current_plugin); - if(content_url.empty()) { - show_notification("Manga", "Url is missing for manga!", Urgency::CRITICAL); - return false; - } - - Path content_storage_dir = get_storage_dir().join(current_plugin->name); + std::unique_ptr Program::create_body() { + return std::make_unique(this, font.get(), bold_font.get(), cjk_font.get()); + } - std::string manga_id; - if(!manga_plugin->extract_id_from_url(content_url, manga_id)) - return false; + std::unique_ptr Program::create_search_bar(const std::string &placeholder, int search_delay) { + auto search_bar = std::make_unique(*font, &plugin_logo, placeholder); + search_bar->text_autosearch_delay = search_delay; + return search_bar; + } - manga_id_base64 = base64_encode(manga_id); - content_storage_file = content_storage_dir.join(manga_id_base64); - content_storage_json.clear(); - content_storage_json["name"] = content_title; - FileType file_type = get_file_type(content_storage_file); - if(file_type == FileType::REGULAR) - read_file_as_json(content_storage_file, content_storage_json); - } else if(next_page == Page::VIDEO_CONTENT) { - watched_videos.clear(); - if(content_url.empty()) - next_page = Page::SEARCH_SUGGESTION; - else { - page_stack.push(Page::SEARCH_SUGGESTION); - } - current_page = next_page; + bool Program::load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id) { + Path content_storage_dir = get_storage_dir().join(service_name); + manga_id_base64 = base64_encode(manga_id); + content_storage_file = content_storage_dir.join(manga_id_base64); + content_storage_json.clear(); + content_storage_json["name"] = manga_title; + FileType file_type = get_file_type(content_storage_file); + if(file_type == FileType::REGULAR) { + if(read_file_as_json(content_storage_file, content_storage_json) && content_storage_json.isObject()) + return true; return false; - } else if(next_page == Page::CONTENT_LIST) { - content_list_url = content_url; - } else if(next_page == Page::IMAGE_BOARD_THREAD_LIST) { - image_board_thread_list_url = content_url; + } else { + return true; } - current_page = next_page; - return true; } - void Program::search_suggestion_page() { - std::string update_search_text; - bool search_text_updated = false; - bool search_running = false; - bool typing = false; - bool is_fourchan = current_plugin->name == "4chan"; - - std::string autocomplete_text; - bool autocomplete_running = false; - - Body history_body(this, font.get(), bold_font.get(), cjk_font.get()); - std::unique_ptr recommended_body; - sf::Text all_tab_text("All", *font, tab_text_size); - sf::Text history_tab_text("History", *font, tab_text_size); - sf::Text recommended_tab_text("Recommended", *font, tab_text_size); - sf::Text login_tab_text("Login", *font, tab_text_size); - SearchBar *focused_login_input = nullptr; - - if(current_plugin->name == "youtube") { - recommended_body = std::make_unique(this, font.get(), bold_font.get(), cjk_font.get()); - recommended_body->draw_thumbnails = true; - fill_recommended_items_from_json(load_recommended_json(current_plugin), recommended_body->items); + void Program::select_file(const std::string &filepath) { + puts(filepath.c_str()); + selected_files.push_back(filepath); + } + + void Program::page_loop(std::vector tabs) { + if(tabs.empty()) { + show_notification("QuickMedia", "No tabs provided!", Urgency::CRITICAL); + return; } - std::vector tabs; - int selected_tab = 0; + const Json::Value *json_chapters = &Json::Value::nullSingleton(); + if(content_storage_json.isObject()) { + const Json::Value &chapters_json = content_storage_json["chapters"]; + if(chapters_json.isObject()) + json_chapters = &chapters_json; + } - auto login_submit_callback = [this, &tabs, &selected_tab](const std::string&) -> bool { - if(!tabs[selected_tab].body) { - std::string username = tabs[selected_tab].login_tab->username->get_text(); - std::string password = tabs[selected_tab].login_tab->password->get_text(); - if(current_plugin->name == "4chan") { - std::string response_msg; - PluginResult result = static_cast(current_plugin)->login(username, password, response_msg); - if(result == PluginResult::NET_ERR) { - show_notification("4chan", "Login failed!", Urgency::CRITICAL); - } else if(result == PluginResult::ERR) { - std::string desc = "Login failed, reason: "; - if(response_msg.empty()) - desc += "Unknown"; - else - desc += response_msg; - show_notification("4chan", desc, Urgency::CRITICAL); - } else if(result == PluginResult::OK) { - show_notification("4chan", "Successfully logged in!", Urgency::LOW); - selected_tab = 0; - } - } - } - return false; + struct TabAssociatedData { + std::string update_search_text; + bool search_text_updated = false; + bool search_running = false; + bool typing = false; + bool fetching_next_page_running = false; + int fetched_page = 0; + sf::Text search_result_text; + std::future search_future; + std::future next_page_future; }; - tabs.push_back(Tab{body, nullptr, SearchSuggestionTab::ALL, &all_tab_text}); - tabs.push_back(Tab{&history_body, nullptr, SearchSuggestionTab::HISTORY, &history_tab_text}); - if(recommended_body) - tabs.push_back(Tab{recommended_body.get(), nullptr, SearchSuggestionTab::RECOMMENDED, &recommended_tab_text}); - if(is_fourchan) { - tabs.push_back(Tab{nullptr, std::make_unique(*font), SearchSuggestionTab::LOGIN, &login_tab_text}); - focused_login_input = tabs.back().login_tab->username.get(); + std::vector tab_associated_data; + for(size_t i = 0; i < tabs.size(); ++i) { + TabAssociatedData data; + data.search_result_text = sf::Text("", *font, 30); + tab_associated_data.push_back(std::move(data)); + } - tabs.back().login_tab->username->caret_visible = true; - tabs.back().login_tab->password->caret_visible = false; + //std::string autocomplete_text; + //bool autocomplete_running = false; - tabs.back().login_tab->username->onTextSubmitCallback = login_submit_callback; - tabs.back().login_tab->password->onTextSubmitCallback = login_submit_callback; - } + double gradient_inc = 0.0; + const float gradient_height = 5.0f; + sf::Vertex gradient_points[4]; + + sf::Text tab_text("", *font, tab_text_size); + int selected_tab = 0; - plugin_get_watch_history(current_plugin, history_body.items); - if(current_plugin->name == "youtube") - history_body.draw_thumbnails = true; + bool loop_running = true; - search_bar->onTextBeginTypingCallback = [&typing]() { - typing = true; - }; + auto submit_handler = [this, &tabs, &selected_tab, &loop_running]() { + BodyItem *selected_item = tabs[selected_tab].body->get_selected(); + if(!selected_item) + return; + + std::vector new_tabs; + PluginResult submit_result = tabs[selected_tab].page->submit(selected_item->get_title(), selected_item->url, new_tabs); + if(submit_result == PluginResult::OK) { + if(tabs[selected_tab].page->is_single_page()) { + tabs[selected_tab].search_bar->clear(); + if(new_tabs.size() == 1) + tabs[selected_tab].body = std::move(new_tabs[0].body); + else + loop_running = false; + return; + } - search_bar->autocomplete_search_delay = current_plugin->get_autocomplete_delay(); - search_bar->onAutocompleteRequestCallback = [this, &tabs, &selected_tab, &autocomplete_text](const std::string &text) { - if(tabs[selected_tab].body == body && !current_plugin->search_is_filter()) - autocomplete_text = text; - }; + if(new_tabs.empty()) + return; - std::string recommended_filter; + for(Tab &tab : tabs) { + tab.body->clear_cache(); + } - search_bar->onTextUpdateCallback = [&update_search_text, &search_text_updated, this, &tabs, &selected_tab, &typing, &recommended_body, &recommended_filter](const std::string &text) { - if(tabs[selected_tab].body == body && !current_plugin->search_is_filter()) { - update_search_text = text; - search_text_updated = true; + if(new_tabs.size() == 1 && new_tabs[0].page->is_manga_images_page()) { + select_episode(selected_item, false); + Body *chapters_body = tabs[selected_tab].body.get(); + chapters_body->filter_search_fuzzy(""); // Needed (or not really) to go to the next chapter when reaching the last page of a chapter + MangaImagesPage *manga_images_page = static_cast(new_tabs[0].page.get()); + window.setKeyRepeatEnabled(false); + while(true) { + if(current_page == PageType::IMAGES) { + window.setFramerateLimit(20); + while(current_page == PageType::IMAGES) { + int page_navigation = image_page(manga_images_page, chapters_body); + if(page_navigation == -1) { + // TODO: Make this work if the list is sorted differently than from newest to oldest. + chapters_body->select_next_item(); + select_episode(chapters_body->get_selected(), true); + image_index = 99999; // Start at the page that shows we are at the end of the chapter + manga_images_page->change_chapter(chapters_body->get_selected()->get_title(), chapters_body->get_selected()->url); + } else if(page_navigation == 1) { + // TODO: Make this work if the list is sorted differently than from newest to oldest. + chapters_body->select_previous_item(); + select_episode(chapters_body->get_selected(), true); + manga_images_page->change_chapter(chapters_body->get_selected()->get_title(), chapters_body->get_selected()->url); + } + } + if(vsync_set) + window.setFramerateLimit(0); + else + window.setFramerateLimit(monitor_hz); + } else if(current_page == PageType::IMAGES_CONTINUOUS) { + image_continuous_page(manga_images_page); + } else { + break; + } + } + window.setKeyRepeatEnabled(true); + } else if(new_tabs.size() == 1 && new_tabs[0].page->is_image_board_thread_page()) { + current_page = PageType::IMAGE_BOARD_THREAD; + image_board_thread_page(static_cast(new_tabs[0].page.get()), new_tabs[0].body.get()); + } else if(new_tabs.size() == 1 && new_tabs[0].page->is_video_page()) { + current_page = PageType::VIDEO_CONTENT; + video_content_page(new_tabs[0].page.get(), selected_item->url, selected_item->get_title()); + } else { + page_loop(std::move(new_tabs)); + } } else { - tabs[selected_tab].body->filter_search_fuzzy(text); - tabs[selected_tab].body->select_first_item(); + // TODO: Show the exact cause of error (get error message from curl). + // TODO: Make asynchronous + show_notification("QuickMedia", std::string("Submit failed for page ") + tabs[selected_tab].page->get_title(), Urgency::CRITICAL); } - if(tabs[selected_tab].body == recommended_body.get()) - recommended_filter = text; - typing = false; }; - search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &typing](const std::string&) -> bool { - if(current_plugin->name != "dmenu") { - if(typing || tabs[selected_tab].body->no_items_visible()) - return false; - } - return on_search_suggestion_submit_text(tabs[selected_tab].body, body); - }; + for(size_t i = 0; i < tabs.size(); ++i) { + Tab &tab = tabs[i]; + TabAssociatedData &associated_data = tab_associated_data[i]; + if(!tab.search_bar) + continue; - if(current_plugin->get_front_page(body->items) != PluginResult::OK) { - show_notification("QuickMedia", "Failed to get front page", Urgency::CRITICAL); - current_page = Page::EXIT; - return; + // tab.search_bar->autocomplete_search_delay = current_plugin->get_autocomplete_delay(); + // tab.search_bar->onAutocompleteRequestCallback = [this, &tabs, &selected_tab, &autocomplete_text](const std::string &text) { + // if(tabs[selected_tab].body == body && !current_plugin->search_is_filter()) + // autocomplete_text = text; + // }; + + tab.search_bar->onTextUpdateCallback = [&associated_data, &tabs, i](const std::string &text) { + if(!tabs[i].page->search_is_filter()) { + associated_data.update_search_text = text; + associated_data.search_text_updated = true; + } else { + tabs[i].body->filter_search_fuzzy(text); + tabs[i].body->select_first_item(); + } + associated_data.typing = false; + }; + + tab.search_bar->onTextSubmitCallback = [&submit_handler, &associated_data](const std::string&) { + if(associated_data.typing) + return; + submit_handler(); + }; } - body->clamp_selection(); sf::Vector2f body_pos; sf::Vector2f body_size; @@ -1103,149 +1053,247 @@ namespace QuickMedia { sf::RoundedRectangleShape tab_background(sf::Vector2f(1.0f, 1.0f), 10.0f, 10); tab_background.setFillColor(tab_selected_color); - while (current_page == Page::SEARCH_SUGGESTION) { + sf::Clock frame_timer; + + while (window.isOpen() && loop_running) { + sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); + while (window.pollEvent(event)) { - base_event_handler(event, Page::EXIT, false, true, tabs[selected_tab].body != nullptr); + if (event.type == sf::Event::Closed) { + window.close(); + } else if(event.type == sf::Event::Resized) { + window_size.x = event.size.width; + window_size.y = event.size.height; + sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); + window.setView(sf::View(visible_area)); + } + + if(tabs[selected_tab].search_bar) { + if(event.type == sf::Event::TextEntered) + tabs[selected_tab].search_bar->onTextEntered(event.text.unicode); + tabs[selected_tab].search_bar->on_event(event); + } + if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) redraw = true; else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Up) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::Down) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_next_item(); + if(event.key.code == sf::Keyboard::Down || event.key.code == sf::Keyboard::PageDown || event.key.code == sf::Keyboard::End) { + bool hit_bottom = false; + switch(event.key.code) { + case sf::Keyboard::Down: + hit_bottom = !tabs[selected_tab].body->select_next_item(); + break; + case sf::Keyboard::PageDown: + hit_bottom = !tabs[selected_tab].body->select_next_page(); + break; + case sf::Keyboard::End: + tabs[selected_tab].body->select_last_item(); + hit_bottom = true; + break; + default: + hit_bottom = false; + break; + } + if(hit_bottom && !tab_associated_data[selected_tab].search_running && !tab_associated_data[selected_tab].fetching_next_page_running && tabs[selected_tab].page) { + gradient_inc = 0.0; + tab_associated_data[selected_tab].fetching_next_page_running = true; + int next_page = tab_associated_data[selected_tab].fetched_page + 1; + Page *page = tabs[selected_tab].page.get(); + std::string update_search_text = tab_associated_data[selected_tab].update_search_text; + tab_associated_data[selected_tab].next_page_future = std::async(std::launch::async, [update_search_text, next_page, page]() { + BodyItems result_items; + if(page->get_page(update_search_text, next_page, result_items) != PluginResult::OK) + fprintf(stderr, "Failed to get next page (page %d)\n", next_page); + return result_items; + }); + } + } else if(event.key.code == sf::Keyboard::Up) { + tabs[selected_tab].body->select_previous_item(); } else if(event.key.code == sf::Keyboard::PageUp) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_previous_page(); - } else if(event.key.code == sf::Keyboard::PageDown) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_next_page(); + tabs[selected_tab].body->select_previous_page(); } else if(event.key.code == sf::Keyboard::Home) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_first_item(); - } else if(event.key.code == sf::Keyboard::End) { - if(tabs[selected_tab].body) tabs[selected_tab].body->select_last_item(); + tabs[selected_tab].body->select_first_item(); } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::EXIT; - exit_code = 1; + goto page_end; } else if(event.key.code == sf::Keyboard::Left) { - if(tabs[selected_tab].body) { - tabs[selected_tab].body->filter_search_fuzzy(""); - tabs[selected_tab].body->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); + if(selected_tab > 0) { + tabs[selected_tab].body->clear_cache(); + --selected_tab; + redraw = true; } - selected_tab = std::max(0, selected_tab - 1); - search_bar->clear(); } else if(event.key.code == sf::Keyboard::Right) { - if(tabs[selected_tab].body) { - tabs[selected_tab].body->filter_search_fuzzy(""); - tabs[selected_tab].body->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); + if(selected_tab < (int)tabs.size() - 1) { + tabs[selected_tab].body->clear_cache(); + ++selected_tab; + redraw = true; } - selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1); - search_bar->clear(); } else if(event.key.code == sf::Keyboard::Tab) { - if(tabs[selected_tab].body) search_bar->set_to_autocomplete(); - } - } - - if(!tabs[selected_tab].body) { - if(event.type == sf::Event::TextEntered) - focused_login_input->onTextEntered(event.text.unicode); - focused_login_input->on_event(event); - - if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::Tab) { - focused_login_input->caret_visible = false; - if(focused_login_input == tabs[selected_tab].login_tab->username.get()) - focused_login_input = tabs[selected_tab].login_tab->password.get(); - else - focused_login_input = tabs[selected_tab].login_tab->username.get(); - focused_login_input->caret_visible = true; + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->set_to_autocomplete(); + } else if(event.key.code == sf::Keyboard::Enter) { + if(!tabs[selected_tab].search_bar) submit_handler(); + } else if(event.key.code == sf::Keyboard::T && event.key.control) { + BodyItem *selected_item = tabs[selected_tab].body->get_selected(); + if(selected_item && tabs[selected_tab].page && tabs[selected_tab].page->is_trackable()) { + TrackablePage *trackable_page = static_cast(tabs[selected_tab].page.get()); + TrackResult track_result = trackable_page->track(selected_item->get_title()); + // TODO: Show proper error message when this fails. For example if we are already tracking the manga + if(track_result == TrackResult::OK) { + show_notification("Media tracker", "You are now tracking \"" + trackable_page->content_title + "\" after \"" + selected_item->get_title() + "\"", Urgency::LOW); + } else { + show_notification("Media tracker", "Failed to track media \"" + trackable_page->content_title + "\", chapter: \"" + selected_item->get_title() + "\"", Urgency::CRITICAL); + } + } } } } if(redraw) { redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size, true); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->onWindowResize(window_size); + // TODO: Dont show tabs if there is only one tab + get_body_dimensions(window_size, tabs[selected_tab].search_bar.get(), body_pos, body_size, true); + + gradient_points[0].position.x = 0.0f; + gradient_points[0].position.y = window_size.y - gradient_height; + + gradient_points[1].position.x = window_size.x; + gradient_points[1].position.y = window_size.y - gradient_height; + + gradient_points[2].position.x = window_size.x; + gradient_points[2].position.y = window_size.y; + + gradient_points[3].position.x = 0.0f; + gradient_points[3].position.y = window_size.y; } - if(tabs[selected_tab].body) - search_bar->update(); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->update(); + + for(size_t i = 0; i < tabs.size(); ++i) { + TabAssociatedData &associated_data = tab_associated_data[i]; - if(search_text_updated && !search_running) { - search_suggestion_future = std::async(std::launch::async, [this, update_search_text]() { - BodyItems result; - if(current_plugin->update_search_suggestions(update_search_text, result) != SuggestionResult::OK) { - show_notification("Search", "Search failed!", Urgency::CRITICAL); + if(associated_data.fetching_next_page_running && associated_data.next_page_future.valid() && associated_data.next_page_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + BodyItems new_body_items = associated_data.next_page_future.get(); + fprintf(stderr, "Finished fetching page %d, num new messages: %zu\n", associated_data.fetched_page + 1, new_body_items.size()); + size_t num_new_messages = new_body_items.size(); + if(num_new_messages > 0) { + tabs[i].body->append_items(std::move(new_body_items)); + associated_data.fetched_page++; } - return result; - }); - update_search_text.clear(); - search_text_updated = false; - search_running = true; - } + associated_data.fetching_next_page_running = false; + } - if(search_running && search_suggestion_future.valid() && search_suggestion_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - if(!search_text_updated) { - body->items = search_suggestion_future.get(); - body->select_first_item(); - } else { - search_suggestion_future.get(); + if(associated_data.search_text_updated && !associated_data.search_running && !associated_data.fetching_next_page_running) { + Page *page = tabs[i].page.get(); + std::string update_search_text = associated_data.update_search_text; + associated_data.search_future = std::async(std::launch::async, [update_search_text, page]() { + BodyItems result_items; + if(page->search(update_search_text, result_items) != SearchResult::OK) { + show_notification("QuickMedia", "Search failed!", Urgency::CRITICAL); + } + return result_items; + }); + update_search_text.clear(); + associated_data.search_text_updated = false; + associated_data.search_running = true; + associated_data.search_result_text.setString("Searching..."); + } + + if(associated_data.search_running && associated_data.search_future.valid() && associated_data.search_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + if(!associated_data.search_text_updated) { + BodyItems result_items = associated_data.search_future.get(); + tabs[i].body->items = std::move(result_items); + tabs[i].body->select_first_item(); + if(tabs[i].body->items.empty()) + associated_data.search_result_text.setString("No results found"); + else + associated_data.search_result_text.setString(""); + } else { + associated_data.search_future.get(); + } + associated_data.search_running = false; } - search_running = false; } - if(!autocomplete_text.empty() && !autocomplete_running) { - autocomplete_future = std::async(std::launch::async, [this, autocomplete_text]() { - return current_plugin->autocomplete_search(autocomplete_text); - }); - autocomplete_text.clear(); - autocomplete_running = true; - } + // if(!autocomplete_text.empty() && !autocomplete_running) { + // autocomplete_future = std::async(std::launch::async, [this, autocomplete_text]() { + // return current_plugin->autocomplete_search(autocomplete_text); + // }); + // autocomplete_text.clear(); + // autocomplete_running = true; + // } - if(autocomplete_running && autocomplete_future.valid() && autocomplete_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - search_bar->set_autocomplete_text(autocomplete_future.get()); - autocomplete_running = false; - } + // if(autocomplete_running && autocomplete_future.valid() && autocomplete_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { + // if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->set_autocomplete_text(autocomplete_future.get()); + // autocomplete_running = false; + // } window.clear(back_color); - if(tabs[selected_tab].body) - search_bar->draw(window, false); + if(tabs[selected_tab].search_bar) tabs[selected_tab].search_bar->draw(window, false); + { + float shade_extra_height = 0.0f; + if(!tabs[selected_tab].search_bar) + shade_extra_height = 10.0f; + const float width_per_tab = window_size.x / tabs.size(); tab_background.setSize(sf::Vector2f(std::floor(width_per_tab - tab_margin_x * 2.0f), tab_height)); - float tab_vertical_offset = search_bar->getBottomWithoutShadow(); - if(tabs[selected_tab].body) { - tabs[selected_tab].body->draw(window, body_pos, body_size); - } else { - tabs[selected_tab].login_tab->username->draw(window, false); - tabs[selected_tab].login_tab->password->draw(window, false); - tabs[selected_tab].login_tab->password->set_vertical_position(tabs[selected_tab].login_tab->username->getBottomWithoutShadow()); - tab_vertical_offset = tabs[selected_tab].login_tab->username->getBottomWithoutShadow() + tabs[selected_tab].login_tab->password->getBottomWithoutShadow(); - } - const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f); + float tab_vertical_offset = tabs[selected_tab].search_bar ? tabs[selected_tab].search_bar->getBottomWithoutShadow() : 0.0f; + tabs[selected_tab].body->draw(window, body_pos, body_size, *json_chapters); + const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f) + shade_extra_height; tab_shade.setPosition(0.0f, tab_spacer_height + std::floor(tab_vertical_offset)); - tab_shade.setSize(sf::Vector2f(window_size.x, tab_height + 10.0f)); + tab_shade.setSize(sf::Vector2f(window_size.x, shade_extra_height + tab_height + 10.0f)); window.draw(tab_shade); int i = 0; + // TODO: Dont show tabs if there is only one tab for(Tab &tab : tabs) { if(i == selected_tab) { - tab_background.setPosition(std::floor(i * width_per_tab + tab_margin_x), tab_spacer_height + std::floor(tab_vertical_offset)); + tab_background.setPosition(std::floor(i * width_per_tab + tab_margin_x), tab_spacer_height + std::floor(tab_vertical_offset) + shade_extra_height); window.draw(tab_background); } const float center = (i * width_per_tab) + (width_per_tab * 0.5f); - tab.text->setPosition(std::floor(center - tab.text->getLocalBounds().width * 0.5f), tab_y); - window.draw(*tab.text); + // TODO: Optimize. Only set once for each tab! + tab_text.setString(tab.page->get_title()); + tab_text.setPosition(std::floor(center - tab_text.getLocalBounds().width * 0.5f), tab_y); + window.draw(tab_text); ++i; } } + if(tab_associated_data[selected_tab].fetching_next_page_running) { + double progress = 0.5 + std::sin(std::fmod(gradient_inc, 360.0) * 0.017453292519943295 - 1.5707963267948966*0.5) * 0.5; + gradient_inc += (frame_time_ms * 0.5); + sf::Color bottom_color = interpolate_colors(back_color, sf::Color(175, 180, 188), progress); + + gradient_points[0].color = back_color; + gradient_points[1].color = back_color; + gradient_points[2].color = bottom_color; + gradient_points[3].color = bottom_color; + window.draw(gradient_points, 4, sf::Quads); // Note: sf::Quads doesn't work with egl + } + + if(!tab_associated_data[selected_tab].search_result_text.getString().isEmpty()) { + auto search_result_text_bounds = tab_associated_data[selected_tab].search_result_text.getLocalBounds(); + tab_associated_data[selected_tab].search_result_text.setPosition( + std::floor(body_pos.x + body_size.x * 0.5f - search_result_text_bounds.width * 0.5f), + std::floor(body_pos.y + body_size.y * 0.5f - search_result_text_bounds.height * 0.5f)); + window.draw(tab_associated_data[selected_tab].search_result_text); + } + window.display(); } - search_bar->onTextBeginTypingCallback = nullptr; - search_bar->onAutocompleteRequestCallback = nullptr; + page_end: + // TODO: This is needed, because you cant terminate futures without causing an exception to be thrown and its not safe anyways. + // Need a way to solve this, we dont want to wait for a search to finish when navigating backwards + for(TabAssociatedData &associated_data : tab_associated_data) { + if(associated_data.next_page_future.valid()) + associated_data.next_page_future.get(); + if(associated_data.search_future.valid()) + associated_data.search_future.get(); + } } static bool youtube_url_extract_id(const std::string &youtube_url, std::string &youtube_video_id) { @@ -1326,17 +1374,17 @@ namespace QuickMedia { return true; } - void Program::save_recommendations_from_related_videos() { + void Program::save_recommendations_from_related_videos(const std::string &video_url, const std::string &video_title, const Body *related_media_body) { std::string video_id; - if(!youtube_url_extract_id(content_url, video_id)) { + if(!youtube_url_extract_id(video_url, video_id)) { std::string err_msg = "Failed to extract id of youtube url "; - err_msg += content_url; + err_msg += video_url; err_msg + ", video wont be saved in recommendations"; show_notification("Video player", err_msg.c_str(), Urgency::LOW); return; } - Json::Value recommended_json = load_recommended_json(current_plugin); + Json::Value recommended_json = load_recommended_json(); time_t time_now = time(NULL); Json::Value &existing_recommended_json = recommended_json[video_id]; @@ -1349,7 +1397,7 @@ namespace QuickMedia { existing_recommended_json["watched_timestamp"] = time_now; } else { Json::Value new_content_object(Json::objectValue); - new_content_object["title"] = content_title; + new_content_object["title"] = video_title; new_content_object["recommended_timestamp"] = time_now; new_content_object["recommended_count"] = 1; new_content_object["watched_count"] = 1; @@ -1381,24 +1429,21 @@ namespace QuickMedia { break; } } else { - fprintf(stderr, "Failed to extract id of youtube url %s, video wont be saved in recommendations\n", content_url.c_str()); + fprintf(stderr, "Failed to extract id of youtube url %s, video wont be saved in recommendations\n", video_url.c_str()); } } - save_json_to_file_atomic(get_recommended_filepath(current_plugin), recommended_json); + save_json_to_file_atomic(get_recommended_filepath(plugin_name), recommended_json); } #define CLEANMASK(mask) ((mask) & (ShiftMask|ControlMask|Mod1Mask|Mod4Mask|Mod5Mask)) - void Program::video_content_page() { - search_bar->onTextUpdateCallback = nullptr; - search_bar->onTextSubmitCallback = nullptr; - + void Program::video_content_page(Page *page, std::string video_url, std::string video_title) { sf::Clock time_watched_timer; bool added_recommendations = false; bool video_loaded = false; - Page previous_page = pop_page_stack(); + PageType previous_page = pop_page_stack(); std::unique_ptr video_player; std::unique_ptr related_media_window; @@ -1408,56 +1453,45 @@ namespace QuickMedia { sf::Text related_videos_text("Related videos", *bold_font, 20); const float related_videos_text_height = related_videos_text.getCharacterSize(); + auto related_media_body = create_body(); + related_media_body->draw_thumbnails = true; + sf::WindowHandle video_player_window = None; - auto on_window_create = [this, &video_player_window, &related_media_window, &related_media_window_size](sf::WindowHandle _video_player_window) mutable { + auto on_window_create = [this, &video_player_window](sf::WindowHandle _video_player_window) mutable { video_player_window = _video_player_window; - - if(!current_plugin->is_image_board()) { - related_media_window_size.x = window_size.x * RELATED_MEDIA_WINDOW_WIDTH; - related_media_window_size.y = window_size.y; - related_media_window = std::make_unique(sf::VideoMode(related_media_window_size.x, related_media_window_size.y), "", 0, sf::ContextSettings(0, 0, 0, 3, 3)); - related_media_window->setFramerateLimit(0); - if(!enable_vsync(disp, related_media_window->getSystemHandle())) { - fprintf(stderr, "Failed to enable vsync, fallback to frame limiting\n"); - related_media_window->setFramerateLimit(monitor_hz); - } - related_media_window->setVisible(false); - XReparentWindow(disp, related_media_window->getSystemHandle(), video_player_window, window_size.x - related_media_window_size.x, 0); - } - XSelectInput(disp, video_player_window, KeyPressMask | PointerMotionMask); XSync(disp, False); }; - auto load_video_error_check = [this, &video_player, previous_page, &time_watched_timer, &added_recommendations]() mutable { + auto load_video_error_check = [this, &related_media_body, &video_url, &video_title, &video_player, previous_page, &time_watched_timer, &added_recommendations, page]() mutable { time_watched_timer.restart(); added_recommendations = false; - watched_videos.insert(content_url); - VideoPlayer::Error err = video_player->load_video(content_url.c_str(), window.getSystemHandle(), current_plugin->name); + watched_videos.insert(video_url); + VideoPlayer::Error err = video_player->load_video(video_url.c_str(), window.getSystemHandle(), plugin_name); if(err != VideoPlayer::Error::OK) { std::string err_msg = "Failed to play url: "; - err_msg += content_url; + err_msg += video_url; show_notification("Video player", err_msg.c_str(), Urgency::CRITICAL); current_page = previous_page; } else { related_media_body->clear_items(); related_media_body->clear_thumbnails(); - related_media_body->items = current_plugin->get_related_media(content_url); + related_media_body->items = page->get_related_media(video_url); // TODO: Make this also work for other video plugins - if(current_plugin->name != "youtube") + if(strcmp(plugin_name, "youtube") != 0) return; std::string video_id; - if(!youtube_url_extract_id(content_url, video_id)) { + if(!youtube_url_extract_id(video_url, video_id)) { std::string err_msg = "Failed to extract id of youtube url "; - err_msg += content_url; + err_msg += video_url; err_msg + ", video wont be saved in history"; show_notification("Video player", err_msg.c_str(), Urgency::LOW); return; } - Json::Value video_history_json = load_video_history_json(current_plugin); + Json::Value video_history_json = load_video_history_json(); int existing_index = watch_history_get_item_by_id(video_history_json, video_id.c_str()); if(existing_index != -1) { @@ -1470,18 +1504,18 @@ namespace QuickMedia { Json::Value new_content_object(Json::objectValue); new_content_object["id"] = video_id; - new_content_object["title"] = content_title; + new_content_object["title"] = video_title; new_content_object["timestamp"] = time_now; video_history_json.append(std::move(new_content_object)); - Path video_history_filepath = get_video_history_filepath(current_plugin); + Path video_history_filepath = get_video_history_filepath(plugin_name); save_json_to_file_atomic(video_history_filepath, video_history_json); } }; bool has_video_started = true; - auto video_event_callback = [this, &video_player, &load_video_error_check, previous_page, &has_video_started, &time_watched_timer, &video_loaded](const char *event_name) mutable { + auto video_event_callback = [this, &related_media_body, &video_url, &video_title, &video_player, &load_video_error_check, previous_page, &has_video_started, &time_watched_timer, &video_loaded](const char *event_name) mutable { bool end_of_file = false; if(strcmp(event_name, "pause") == 0) { double time_remaining = 9999.0; @@ -1518,8 +1552,8 @@ namespace QuickMedia { return; } - content_url = std::move(new_video_url); - content_title = std::move(new_video_title); + video_url = std::move(new_video_url); + video_title = std::move(new_video_title); load_video_error_check(); } }; @@ -1540,27 +1574,28 @@ namespace QuickMedia { bool cursor_visible = true; sf::Clock cursor_hide_timer; - bool is_youtube = current_plugin->name == "youtube"; - bool is_pornhub = current_plugin->name == "pornhub"; + bool is_youtube = strcmp(plugin_name, "youtube") == 0; + bool is_pornhub = strcmp(plugin_name, "pornhub") == 0; bool supports_url_timestamp = is_youtube || is_pornhub; - auto save_video_url_to_clipboard = [this, &video_player_window, &video_player, &supports_url_timestamp]() { + auto save_video_url_to_clipboard = [&video_url, &video_player_window, &video_player, &supports_url_timestamp]() { if(!video_player_window) return; if(supports_url_timestamp) { + // TODO: Remove timestamp (&t= or ?t=) from video_url double time_in_file; if(video_player->get_time_in_file(&time_in_file) != VideoPlayer::Error::OK) time_in_file = 0.0; - sf::Clipboard::setString(content_url + "&t=" + std::to_string((int)time_in_file)); + sf::Clipboard::setString(video_url + "&t=" + std::to_string((int)time_in_file)); } else { - sf::Clipboard::setString(content_url); + sf::Clipboard::setString(video_url); } }; - while (current_page == Page::VIDEO_CONTENT) { + while (current_page == PageType::VIDEO_CONTENT) { while (window.pollEvent(event)) { - base_event_handler(event, previous_page, true, false, false); + base_event_handler(event, previous_page, related_media_body.get(), nullptr, true, false); if(event.type == sf::Event::Resized && related_media_window) { related_media_window_size.x = window_size.x * RELATED_MEDIA_WINDOW_WIDTH; related_media_window_size.y = window_size.y; @@ -1605,8 +1640,8 @@ namespace QuickMedia { related_media_window->setVisible(false); has_video_started = false; - content_url = selected_item->url; - content_title = selected_item->get_title(); + video_url = selected_item->url; + video_title = selected_item->get_title(); load_video_error_check(); } else if(event.key.code == sf::Keyboard::C && event.key.control) { save_video_url_to_clipboard(); @@ -1624,7 +1659,21 @@ namespace QuickMedia { current_page = previous_page; } else if(pressed_keysym == XK_f && pressing_ctrl) { window_set_fullscreen(disp, window.getSystemHandle(), WindowFullscreenState::TOGGLE); - } else if(pressed_keysym == XK_r && pressing_ctrl && related_media_window) { + } else if(pressed_keysym == XK_r && pressing_ctrl && strcmp(plugin_name, "4chan") != 0) { + if(!related_media_window) { + related_media_window_size.x = window_size.x * RELATED_MEDIA_WINDOW_WIDTH; + related_media_window_size.y = window_size.y; + related_media_window = std::make_unique(sf::VideoMode(related_media_window_size.x, related_media_window_size.y), "", 0, sf::ContextSettings(0, 0, 0, 3, 3)); + related_media_window->setFramerateLimit(0); + if(!enable_vsync(disp, related_media_window->getSystemHandle())) { + fprintf(stderr, "Failed to enable vsync, fallback to frame limiting\n"); + related_media_window->setFramerateLimit(monitor_hz); + } + related_media_window->setVisible(false); + XReparentWindow(disp, related_media_window->getSystemHandle(), video_player_window, window_size.x - related_media_window_size.x, 0); + XSync(disp, False); + } + related_media_window_visible = true; related_media_window->setVisible(related_media_window_visible); if(!cursor_visible) @@ -1663,7 +1712,7 @@ namespace QuickMedia { /* Only save recommendations for the video if we have been watching it for 15 seconds */ if(is_youtube && video_loaded && !added_recommendations && time_watched_timer.getElapsedTime().asSeconds() >= 15) { added_recommendations = true; - save_recommendations_from_related_videos(); + save_recommendations_from_related_videos(video_url, video_title, related_media_body.get()); } if(video_player_window) { @@ -1701,247 +1750,39 @@ namespace QuickMedia { window_size.y = window_size_u.y; } - enum class TrackMediaType { - RSS, - HTML - }; - - const char* track_media_type_string(TrackMediaType media_type) { - switch(media_type) { - case TrackMediaType::RSS: - return "rss"; - case TrackMediaType::HTML: - return "html"; - } - assert(false); - return ""; - } - - static int track_media(TrackMediaType media_type, const std::string &manga_title, const std::string &chapter_title, const std::string &url) { - const char *args[] = { "automedia", "add", track_media_type_string(media_type), url.data(), "--start-after", chapter_title.data(), "--name", manga_title.data(), nullptr }; - return exec_program(args, nullptr, nullptr); - } - void Program::select_episode(BodyItem *item, bool start_from_beginning) { - images_url = item->url; - chapter_title = item->get_title(); image_index = 0; switch(image_view_mode) { case ImageViewMode::SINGLE: - current_page = Page::IMAGES; + current_page = PageType::IMAGES; break; case ImageViewMode::SCROLL: - current_page = Page::IMAGES_CONTINUOUS; - break; - } - - if(start_from_beginning) - return; - - const Json::Value &json_chapters = content_storage_json["chapters"]; - if(json_chapters.isObject()) { - const Json::Value &json_chapter = json_chapters[chapter_title]; - if(json_chapter.isObject()) { - const Json::Value ¤t = json_chapter["current"]; - if(current.isNumeric()) - image_index = current.asInt() - 1; - } - } - } - - Page Program::pop_page_stack() { - if(!page_stack.empty()) { - Page previous_page = page_stack.top(); - page_stack.pop(); - return previous_page; - } - return Page::EXIT; - } - - enum class EpisodeListTabType { - CHAPTERS, - CREATOR - }; - - struct EpisodeListTab { - EpisodeListTabType type; - Body *body; - const Creator *creator; - std::future creator_page_download_future; - sf::Text text; - }; - - void Program::episode_list_page() { - assert(current_plugin->is_manga()); - Manga *manga = static_cast(current_plugin); - - Json::Value *json_chapters = &content_storage_json["chapters"]; - std::vector tabs; - int selected_tab = 0; - - search_bar->onTextUpdateCallback = [&tabs, &selected_tab](const std::string &text) { - tabs[selected_tab].body->filter_search_fuzzy(text); - tabs[selected_tab].body->select_first_item(); - }; - - search_bar->onTextSubmitCallback = [this, &tabs, &selected_tab, &json_chapters](const std::string&) -> bool { - if(tabs[selected_tab].type == EpisodeListTabType::CHAPTERS) { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; - - select_episode(selected_item, false); - return true; - } else { - if(on_search_suggestion_submit_text(tabs[selected_tab].body, body)) { - selected_tab = 0; - json_chapters = &content_storage_json["chapters"]; - return true; - } else { - return false; - } - } - }; - - auto download_creator_page = [manga](std::string url) { - BodyItems body_items; - if(manga->get_creators_manga_list(url, body_items) != PluginResult::OK) - show_notification("Manga", "Failed to download authors page", Urgency::CRITICAL); - return body_items; - }; - - EpisodeListTab chapters_tab; - chapters_tab.type = EpisodeListTabType::CHAPTERS; - chapters_tab.body = body; - chapters_tab.creator = nullptr; - chapters_tab.text = sf::Text("Chapters", *font, tab_text_size); - tabs.push_back(std::move(chapters_tab)); - - const std::vector& creators = manga->get_creators(); - for(const Creator &creator : creators) { - EpisodeListTab tab; - tab.type = EpisodeListTabType::CREATOR; - tab.body = new Body(this, font.get(), bold_font.get(), cjk_font.get()); - tab.body->draw_thumbnails = true; - tab.creator = &creator; - tab.creator_page_download_future = std::async(std::launch::async, download_creator_page, creator.url); - tab.text = sf::Text(creator.name, *font, tab_text_size); - tabs.push_back(std::move(tab)); - } - - const float tab_spacer_height = 0.0f; - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - - sf::RectangleShape tab_shade; - tab_shade.setFillColor(sf::Color(33, 38, 44)); - - sf::RoundedRectangleShape tab_background(sf::Vector2f(1.0f, 1.0f), 10.0f, 10); - tab_background.setFillColor(tab_selected_color); - - while (current_page == Page::EPISODE_LIST) { - while (window.pollEvent(event)) { - base_event_handler(event, Page::SEARCH_SUGGESTION, false, true); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) - redraw = true; - else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::T && event.key.control && tabs[selected_tab].type == EpisodeListTabType::CHAPTERS) { - BodyItem *selected_item = body->get_selected(); - if(selected_item) { - if(track_media(TrackMediaType::HTML, content_title, selected_item->get_title(), content_url) == 0) { - show_notification("Media tracker", "You are now tracking \"" + content_title + "\" after \"" + selected_item->get_title() + "\"", Urgency::LOW); - } else { - show_notification("Media tracker", "Failed to track media \"" + content_title + "\", chapter: \"" + selected_item->get_title() + "\"", Urgency::CRITICAL); - } - } - } else if(event.key.code == sf::Keyboard::Up) { - tabs[selected_tab].body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::Down) { - tabs[selected_tab].body->select_next_item(); - } else if(event.key.code == sf::Keyboard::PageUp) { - tabs[selected_tab].body->select_previous_page(); - } else if(event.key.code == sf::Keyboard::PageDown) { - tabs[selected_tab].body->select_next_page(); - } else if(event.key.code == sf::Keyboard::Home) { - tabs[selected_tab].body->select_first_item(); - } else if(event.key.code == sf::Keyboard::End) { - tabs[selected_tab].body->select_last_item(); - } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::SEARCH_SUGGESTION; - body->clear_items(); - body->reset_selected(); - search_bar->clear(); - } else if(event.key.code == sf::Keyboard::Left) { - tabs[selected_tab].body->filter_search_fuzzy(""); - tabs[selected_tab].body->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); - selected_tab = std::max(0, selected_tab - 1); - search_bar->clear(); - } else if(event.key.code == sf::Keyboard::Right) { - tabs[selected_tab].body->filter_search_fuzzy(""); - tabs[selected_tab].body->select_first_item(); - tabs[selected_tab].body->clear_thumbnails(); - selected_tab = std::min((int)tabs.size() - 1, selected_tab + 1); - search_bar->clear(); - } - } - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size, true); - } - - search_bar->update(); - - window.clear(back_color); - search_bar->draw(window, false); - - const float width_per_tab = window_size.x / tabs.size(); - tab_background.setSize(sf::Vector2f(std::floor(width_per_tab - tab_margin_x * 2.0f), tab_height)); - - float tab_vertical_offset = search_bar->getBottomWithoutShadow(); - if(tabs[selected_tab].type == EpisodeListTabType::CHAPTERS) - tabs[selected_tab].body->draw(window, body_pos, body_size, *json_chapters); - else - tabs[selected_tab].body->draw(window, body_pos, body_size); - const float tab_y = tab_spacer_height + std::floor(tab_vertical_offset + tab_height * 0.5f - (tab_text_size + 5.0f) * 0.5f); - - tab_shade.setPosition(0.0f, tab_spacer_height + std::floor(tab_vertical_offset)); - tab_shade.setSize(sf::Vector2f(window_size.x, tab_height + 10.0f)); - window.draw(tab_shade); + current_page = PageType::IMAGES_CONTINUOUS; + break; + } - int i = 0; - for(EpisodeListTab &tab : tabs) { - if(tab.type == EpisodeListTabType::CREATOR - && tab.creator_page_download_future.valid() - && tab.creator_page_download_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) - { - tab.body->items = tab.creator_page_download_future.get(); - tab.body->filter_search_fuzzy(search_bar->get_text()); - tab.body->select_first_item(); - } + if(start_from_beginning) + return; - if(i == selected_tab) { - tab_background.setPosition(std::floor(i * width_per_tab + tab_margin_x), tab_spacer_height + std::floor(tab_vertical_offset)); - window.draw(tab_background); - } - const float center = (i * width_per_tab) + (width_per_tab * 0.5f); - tab.text.setPosition(std::floor(center - tab.text.getLocalBounds().width * 0.5f), tab_y); - window.draw(tab.text); - ++i; + const Json::Value &json_chapters = content_storage_json["chapters"]; + if(json_chapters.isObject()) { + const Json::Value &json_chapter = json_chapters[item->get_title()]; + if(json_chapter.isObject()) { + const Json::Value ¤t = json_chapter["current"]; + if(current.isNumeric()) + image_index = current.asInt() - 1; } - - window.display(); } + } - for(EpisodeListTab &tab : tabs) { - if(tab.type == EpisodeListTabType::CREATOR) - delete tab.body; + // TODO: Remove + PageType Program::pop_page_stack() { + if(!page_stack.empty()) { + PageType previous_page = page_stack.top(); + page_stack.pop(); + return previous_page; } + return PageType::EXIT; } // TODO: Optimize this somehow. One image alone uses more than 20mb ram! Total ram usage for viewing one image @@ -1980,24 +1821,23 @@ namespace QuickMedia { } } - // TODO: Cancel download when navigating to another non-manga page - void Program::download_chapter_images_if_needed(Manga *image_plugin) { - if(downloading_chapter_url == images_url) + void Program::download_chapter_images_if_needed(MangaImagesPage *images_page) { + if(downloading_chapter_url == images_page->get_url()) return; - downloading_chapter_url = images_url; + downloading_chapter_url = images_page->get_url(); if(image_download_future.valid()) { + // TODO: Cancel download instead of waiting for the last page to finish image_download_cancel = true; image_download_future.get(); image_download_cancel = false; } - std::string chapter_url = images_url; Path content_cache_dir_ = content_cache_dir; - image_download_future = std::async(std::launch::async, [chapter_url, image_plugin, content_cache_dir_, this]() { + image_download_future = std::async(std::launch::async, [images_page, content_cache_dir_, this]() { // TODO: Download images in parallel int page = 1; - image_plugin->for_each_page_in_chapter(chapter_url, [content_cache_dir_, &page, this](const std::string &url) { + images_page->for_each_page_in_chapter([content_cache_dir_, &page, images_page, this](const std::string &url) { if(image_download_cancel) return false; @@ -2019,7 +1859,7 @@ namespace QuickMedia { return true; std::vector extra_args; - if(current_plugin->name == "manganelo") { + if(strcmp(images_page->get_service_name(), "manganelo") == 0) { extra_args = { CommandArg { "-H", "accept: image/webp,image/apng,image/*,*/*;q=0.8" }, CommandArg { "-H", "sec-fetch-site: cross-site" }, @@ -2029,9 +1869,10 @@ namespace QuickMedia { }; } + // TODO: Download directly to file instead. TODO: Move to page std::string image_content; - if(download_to_string(url, image_content, extra_args, current_plugin->use_tor, true) != DownloadResult::OK || image_content.size() <= 255) { - if(current_plugin->name == "manganelo") { + if(download_to_string(url, image_content, extra_args, is_tor_enabled(), true) != DownloadResult::OK || image_content.size() <= 255) { + if(strcmp(images_page->get_service_name(), "manganelo") == 0) { bool try_backup_url = false; std::string new_url = url; if(string_replace_all(new_url, "s3.mkklcdnv3.com", "bu.mkklcdnbuv1.com") > 0) { @@ -2042,7 +1883,7 @@ namespace QuickMedia { if(try_backup_url) { image_content.clear(); - if(download_to_string(new_url, image_content, extra_args, current_plugin->use_tor, true) != DownloadResult::OK || image_content.size() <= 255) { + if(download_to_string(new_url, image_content, extra_args, is_tor_enabled(), true) != DownloadResult::OK || image_content.size() <= 255) { show_notification("Manganelo", "Failed to download image: " + new_url, Urgency::CRITICAL); return false; } @@ -2109,40 +1950,36 @@ namespace QuickMedia { }); } - void Program::image_page() { + int Program::image_page(MangaImagesPage *images_page, Body *chapters_body) { + int page_navigation = 0; image_download_cancel = false; - search_bar->onTextUpdateCallback = nullptr; - search_bar->onTextSubmitCallback = nullptr; sf::Texture image_texture; sf::Sprite image; sf::Text error_message("", *font, 30); error_message.setFillColor(sf::Color::White); - assert(current_plugin->is_manga()); - Manga *image_plugin = static_cast(current_plugin); - std::string image_data; bool download_in_progress = false; - content_cache_dir = get_cache_dir().join(image_plugin->name).join(manga_id_base64).join(base64_encode(chapter_title)); + content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_encode(images_page->get_chapter_name())); if(create_directory_recursive(content_cache_dir) != 0) { show_notification("Storage", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; - return; + current_page = pop_page_stack(); + return 0; } int num_images = 0; - if(image_plugin->get_number_of_images(images_url, num_images) != ImageResult::OK) { + if(images_page->get_number_of_images(num_images) != ImageResult::OK) { show_notification("Plugin", "Failed to get number of images", Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; - return; + current_page = pop_page_stack(); + return 0; } image_index = std::min(image_index, num_images); if(num_images != (int)image_upscale_status.size()) image_upscale_status.resize(num_images); - download_chapter_images_if_needed(image_plugin); + download_chapter_images_if_needed(images_page); if(image_index < num_images) { sf::String error_msg; @@ -2153,14 +1990,15 @@ namespace QuickMedia { download_in_progress = true; error_message.setString(error_msg); } else if(image_index == num_images) { - error_message.setString("End of " + chapter_title); + error_message.setString("End of " + images_page->get_chapter_name()); } + // TODO: Dont do this every time we change page? Json::Value &json_chapters = content_storage_json["chapters"]; Json::Value json_chapter; int latest_read = image_index + 1; if(json_chapters.isObject()) { - json_chapter = json_chapters[chapter_title]; + json_chapter = json_chapters[images_page->get_chapter_name()]; if(json_chapter.isObject()) { const Json::Value ¤t = json_chapter["current"]; if(current.isNumeric()) @@ -2174,7 +2012,7 @@ namespace QuickMedia { } json_chapter["current"] = std::min(latest_read, num_images); json_chapter["total"] = num_images; - json_chapters[chapter_title] = json_chapter; + json_chapters[images_page->get_chapter_name()] = json_chapter; if(!save_json_to_file_atomic(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } @@ -2182,9 +2020,9 @@ namespace QuickMedia { bool error = !error_message.getString().isEmpty(); bool redraw = true; - sf::Text chapter_text(content_title + " | " + chapter_title + " | Page " + std::to_string(image_index + 1) + "/" + std::to_string(num_images), *font, 14); + sf::Text chapter_text(images_page->manga_name + " | " + images_page->get_chapter_name() + " | Page " + std::to_string(image_index + 1) + "/" + std::to_string(num_images), *font, 14); if(image_index == num_images) - chapter_text.setString(content_title + " | " + chapter_title + " | End"); + chapter_text.setString(images_page->manga_name + " | " + images_page->get_chapter_name() + " | End"); chapter_text.setFillColor(sf::Color::White); sf::RectangleShape chapter_text_background; chapter_text_background.setFillColor(sf::Color(0, 0, 0, 150)); @@ -2204,10 +2042,11 @@ namespace QuickMedia { while(window.pollEvent(event)) {} // TODO: Show to user if a certain page is missing (by checking page name (number) and checking if some are skipped) - while (current_page == Page::IMAGES) { + while (current_page == PageType::IMAGES) { while(window.pollEvent(event)) { if (event.type == sf::Event::Closed) { - current_page = Page::EXIT; + current_page = PageType::EXIT; + window.close(); } else if(event.type == sf::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; @@ -2221,29 +2060,22 @@ namespace QuickMedia { if(image_index > 0) { --image_index; goto end_of_images_page; - } else if(image_index == 0 && body->get_selected_item() < (int)body->items.size() - 1) { - // TODO: Make this work if the list is sorted differently than from newest to oldest. - body->filter_search_fuzzy(""); - body->select_next_item(); - select_episode(body->items[body->get_selected_item()].get(), true); - image_index = 99999; // Start at the page that shows we are at the end of the chapter + } else if(image_index == 0 && chapters_body->get_selected_item() < (int)chapters_body->items.size() - 1) { + page_navigation = -1; goto end_of_images_page; } } else if(event.key.code == sf::Keyboard::Down) { if(image_index < num_images) { ++image_index; goto end_of_images_page; - } else if(image_index == num_images && body->get_selected_item() > 0) { - // TODO: Make this work if the list is sorted differently than from newest to oldest. - body->filter_search_fuzzy(""); - body->select_previous_item(); - select_episode(body->items[body->get_selected_item()].get(), true); + } else if(image_index == num_images && chapters_body->get_selected_item() > 0) { + page_navigation = 1; goto end_of_images_page; } } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); } else if(event.key.code == sf::Keyboard::I) { - current_page = Page::IMAGES_CONTINUOUS; + current_page = PageType::IMAGES_CONTINUOUS; image_view_mode = ImageViewMode::SCROLL; } else if(event.key.code == sf::Keyboard::F) { fit_image_to_window = !fit_image_to_window; @@ -2317,46 +2149,47 @@ namespace QuickMedia { } end_of_images_page: - if(current_page != Page::IMAGES && current_page != Page::IMAGES_CONTINUOUS) { + if(current_page != PageType::IMAGES && current_page != PageType::IMAGES_CONTINUOUS) { image_download_cancel = true; + if(image_download_future.valid()) { + // TODO: Cancel download instead of waiting for the last page to finish + image_download_future.get(); + image_download_cancel = false; + } std::unique_lock lock(image_upscale_mutex); images_to_upscale.clear(); image_upscale_status.clear(); } + return page_navigation; } - void Program::image_continuous_page() { + void Program::image_continuous_page(MangaImagesPage *images_page) { image_download_cancel = false; - search_bar->onTextUpdateCallback = nullptr; - search_bar->onTextSubmitCallback = nullptr; - - assert(current_plugin->is_manga()); - Manga *image_plugin = static_cast(current_plugin); - content_cache_dir = get_cache_dir().join(image_plugin->name).join(manga_id_base64).join(base64_encode(chapter_title)); + content_cache_dir = get_cache_dir().join(images_page->get_service_name()).join(manga_id_base64).join(base64_encode(images_page->get_chapter_name())); if(create_directory_recursive(content_cache_dir) != 0) { show_notification("Storage", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); return; } int num_images = 0; - if(image_plugin->get_number_of_images(images_url, num_images) != ImageResult::OK) { + if(images_page->get_number_of_images(num_images) != ImageResult::OK) { show_notification("Plugin", "Failed to get number of images", Urgency::CRITICAL); - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); return; } if(num_images != (int)image_upscale_status.size()) image_upscale_status.resize(num_images); - download_chapter_images_if_needed(image_plugin); + download_chapter_images_if_needed(images_page); Json::Value &json_chapters = content_storage_json["chapters"]; Json::Value json_chapter; int latest_read = 1 + image_index; if(json_chapters.isObject()) { - json_chapter = json_chapters[chapter_title]; + json_chapter = json_chapters[images_page->get_chapter_name()]; if(json_chapter.isObject()) { const Json::Value ¤t = json_chapter["current"]; if(current.isNumeric()) @@ -2369,27 +2202,27 @@ namespace QuickMedia { json_chapter = Json::Value(Json::objectValue); } - ImageViewer image_viewer(image_plugin, images_url, content_title, chapter_title, image_index, content_cache_dir, font.get()); + ImageViewer image_viewer(images_page, images_page->manga_name, images_page->get_chapter_name(), image_index, content_cache_dir, font.get()); json_chapter["current"] = std::min(latest_read, image_viewer.get_num_pages()); json_chapter["total"] = image_viewer.get_num_pages(); - json_chapters[chapter_title] = json_chapter; + json_chapters[images_page->get_chapter_name()] = json_chapter; if(!save_json_to_file_atomic(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } - while(current_page == Page::IMAGES_CONTINUOUS) { + while(current_page == PageType::IMAGES_CONTINUOUS) { window.clear(back_color); ImageViewerAction action = image_viewer.draw(window); switch(action) { case ImageViewerAction::NONE: break; case ImageViewerAction::RETURN: - current_page = Page::EPISODE_LIST; + current_page = pop_page_stack(); break; case ImageViewerAction::SWITCH_TO_SINGLE_IMAGE_MODE: image_view_mode = ImageViewMode::SINGLE; - current_page = Page::IMAGES; + current_page = PageType::IMAGES; break; } window.display(); @@ -2399,414 +2232,31 @@ namespace QuickMedia { if(focused_page > latest_read) { latest_read = focused_page; json_chapter["current"] = latest_read; - json_chapters[chapter_title] = json_chapter; + json_chapters[images_page->get_chapter_name()] = json_chapter; if(!save_json_to_file_atomic(content_storage_file, content_storage_json)) { show_notification("Manga progress", "Failed to save manga progress", Urgency::CRITICAL); } } } - if(current_page != Page::IMAGES && current_page != Page::IMAGES_CONTINUOUS) { + if(current_page != PageType::IMAGES && current_page != PageType::IMAGES_CONTINUOUS) { image_download_cancel = true; + if(image_download_future.valid()) { + // TODO: Cancel download instead of waiting for the last page to finish + image_download_future.get(); + image_download_cancel = false; + } std::unique_lock lock(image_upscale_mutex); images_to_upscale.clear(); image_upscale_status.clear(); } } - void Program::content_list_page() { - std::string update_search_text; - bool search_text_updated = false; - bool search_running = false; - std::future search_future; - - if(!current_plugin->content_list_search_is_filter()) - search_bar->text_autosearch_delay = current_plugin->get_content_list_search_delay(); - - body->clear_items(); - body->clear_thumbnails(); - if(current_plugin->get_content_list(content_list_url, body->items) != PluginResult::OK) { - show_notification("Content list", "Failed to get content list for url: " + content_list_url, Urgency::CRITICAL); - current_page = Page::SEARCH_SUGGESTION; - return; - } - - search_bar->onTextUpdateCallback = [this, &update_search_text, &search_text_updated](const std::string &text) { - if(current_plugin->content_list_search_is_filter()) { - body->filter_search_fuzzy(text); - body->select_first_item(); - } else { - update_search_text = text; - search_text_updated = true; - } - }; - - search_bar->onTextSubmitCallback = [this](const std::string&) -> bool { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; - - content_episode = selected_item->get_title(); - content_url = selected_item->url; - current_page = Page::CONTENT_DETAILS; - body->clear_items(); - return true; - }; - - int fetched_page = 0; - bool fetching_next_page_running = false; - double gradient_inc = 0; - const float gradient_height = 5.0f; - sf::Vertex gradient_points[4]; - std::future next_page_future; - - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - sf::Clock frame_timer; - - while (current_page == Page::CONTENT_LIST) { - sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); - - while (window.pollEvent(event)) { - base_event_handler(event, Page::SEARCH_SUGGESTION, false); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) { - redraw = true; - } else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Down || event.key.code == sf::Keyboard::PageDown || event.key.code == sf::Keyboard::End) { - bool hit_bottom = false; - switch(event.key.code) { - case sf::Keyboard::Down: - hit_bottom = !body->select_next_item(); - break; - case sf::Keyboard::PageDown: - hit_bottom = !body->select_next_page(); - break; - case sf::Keyboard::End: - body->select_last_item(); - hit_bottom = true; - break; - default: - hit_bottom = false; - break; - } - if(hit_bottom && !search_running && !fetching_next_page_running) { - gradient_inc = 0; - fetching_next_page_running = true; - int next_page = fetched_page + 1; - std::string content_list_url_copy = content_list_url; - std::string update_search_text_copy = update_search_text; - next_page_future = std::async(std::launch::async, [this, content_list_url_copy, update_search_text_copy, next_page]() { - BodyItems result_items; - if(current_plugin->content_list_search_page(content_list_url_copy, update_search_text_copy, next_page, result_items) != SearchResult::OK) - fprintf(stderr, "Failed to get next content list page (page %d)\n", next_page); - return result_items; - }); - } - } else if(event.key.code == sf::Keyboard::Up) { - body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::PageUp) { - body->select_previous_page(); - } else if(event.key.code == sf::Keyboard::Home) { - body->select_first_item(); - } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::SEARCH_SUGGESTION; - body->clear_items(); - body->reset_selected(); - search_bar->clear(); - } - } - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - - gradient_points[0].position.x = 0.0f; - gradient_points[0].position.y = window_size.y - gradient_height; - - gradient_points[1].position.x = window_size.x; - gradient_points[1].position.y = window_size.y - gradient_height; - - gradient_points[2].position.x = window_size.x; - gradient_points[2].position.y = window_size.y; - - gradient_points[3].position.x = 0.0f; - gradient_points[3].position.y = window_size.y; - } - - search_bar->update(); - - if(search_text_updated && !fetching_next_page_running && !search_running) { - std::string search_term = update_search_text; - search_future = std::async(std::launch::async, [this, search_term]() { - BodyItems result; - if(current_plugin->content_list_search(content_list_url, search_term, result) != SearchResult::OK) { - // TODO: Show this? - //show_notification("Search", "Search failed!", Urgency::CRITICAL); - } - return result; - }); - search_text_updated = false; - search_running = true; - } - - if(search_running && search_future.valid() && search_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - if(!search_text_updated) { - body->items = search_future.get(); - body->select_first_item(); - } else { - search_future.get(); - } - search_running = false; - fetched_page = 0; - } - - if(fetching_next_page_running && next_page_future.valid() && next_page_future.wait_for(std::chrono::seconds(0)) == std::future_status::ready) { - BodyItems new_body_items = next_page_future.get(); - fprintf(stderr, "Finished fetching page %d, num new messages: %zu\n", fetched_page + 1, new_body_items.size()); - size_t num_new_messages = new_body_items.size(); - if(num_new_messages > 0) { - body->append_items(std::move(new_body_items)); - fetched_page++; - } - fetching_next_page_running = false; - } - - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - if(fetching_next_page_running) { - double progress = 0.5 + std::sin(std::fmod(gradient_inc, 360.0) * 0.017453292519943295 - 1.5707963267948966*0.5) * 0.5; - gradient_inc += (frame_time_ms * 0.5); - sf::Color bottom_color = interpolate_colors(back_color, sf::Color(175, 180, 188), progress); - - gradient_points[0].color = back_color; - gradient_points[1].color = back_color; - gradient_points[2].color = bottom_color; - gradient_points[3].color = bottom_color; - window.draw(gradient_points, 4, sf::Quads); // Note: sf::Quads doesn't work with egl - } - window.display(); - } - - search_bar->text_autosearch_delay = current_plugin->get_search_delay(); - } - - void Program::content_details_page() { - if(current_plugin->get_content_details(content_list_url, content_url, body->items) != PluginResult::OK) { - show_notification("Content details", "Failed to get content details for url: " + content_url, Urgency::CRITICAL); - // TODO: This will return to an empty content list. - // Each page should have its own @Body so we can return to the last page and still have the data loaded - // however the cached images should be cleared. - current_page = Page::CONTENT_LIST; - return; - } - - search_bar->onTextUpdateCallback = nullptr; - - search_bar->onTextSubmitCallback = [this](const std::string&) -> bool { - if(current_plugin->name == "nyaa.si") { - BodyItem *selected_item = body->get_selected(); - if(selected_item && strncmp(selected_item->url.c_str(), "magnet:?", 8) == 0) { - if(!is_program_executable_by_name("xdg-open")) { - show_notification("Nyaa.si", "xdg-utils which provides xdg-open needs to be installed to download torrents", Urgency::CRITICAL); - return false; - } - const char *args[] = { "xdg-open", selected_item->url.c_str(), nullptr }; - exec_program_async(args, nullptr); - } - } - return false; - }; - - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - - while (current_page == Page::CONTENT_DETAILS) { - while (window.pollEvent(event)) { - base_event_handler(event, Page::CONTENT_LIST); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) - redraw = true; - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - } - - search_bar->update(); - - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - window.display(); - } - } - - void Program::file_manager_page() { - selected_files.clear(); - search_bar->clear(); - int prev_autosearch_delay = search_bar->text_autosearch_delay; - search_bar->text_autosearch_delay = file_manager->get_search_delay(); - Page previous_page = pop_page_stack(); - - sf::Text current_dir_text(file_manager->get_current_dir().string(), *bold_font, 18); - - // TODO: Make asynchronous. - // TODO: Automatically go to the parent if this fails (recursively). - body->select_first_item(); - body->items.clear(); - if(file_manager->get_files_in_directory(body->items) != PluginResult::OK) { - show_notification("QuickMedia", "File manager failed to get files in directory: " + file_manager->get_current_dir().string(), Urgency::CRITICAL); - } - - search_bar->onTextUpdateCallback = [this](const std::string &text) { - body->filter_search_fuzzy(text); - body->reset_selected(); - }; - - search_bar->onTextSubmitCallback = [this, previous_page, ¤t_dir_text](const std::string&) -> bool { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; - - if(file_manager->set_child_directory(selected_item->get_title())) { - std::string current_dir_str = file_manager->get_current_dir().string(); - current_dir_text.setString(current_dir_str); - // TODO: Make asynchronous. - // TODO: Automatically go to the parent if this fails (recursively). - body->items.clear(); - if(file_manager->get_files_in_directory(body->items) != PluginResult::OK) { - show_notification("QuickMedia", "File manager failed to get files in directory: " + current_dir_str, Urgency::CRITICAL); - } - body->select_first_item(); - return true; - } else { - std::filesystem::path full_path = file_manager->get_current_dir() / selected_item->get_title(); - selected_files.push_back(full_path.string()); - printf("%s\n", selected_files.back().c_str()); - current_page = previous_page; - return false; - } - }; - - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - - while (current_page == Page::FILE_MANAGER) { - while (window.pollEvent(event)) { - base_event_handler(event, previous_page); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) - redraw = true; - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - const float dir_text_height = std::floor(current_dir_text.getLocalBounds().height + 12.0f); - body_pos.y += dir_text_height; - body_size.y -= dir_text_height; - current_dir_text.setPosition(body_pos.x, body_pos.y - dir_text_height); - } - - search_bar->update(); - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - window.draw(current_dir_text); - window.display(); - } - - search_bar->text_autosearch_delay = prev_autosearch_delay; - // We want exit code 1 if the file manager was launched and no files were selected, to know when the user didn't select any file(s) - if(selected_files.empty() && current_page == Page::EXIT) - exit_code = 1; - } - - void Program::image_board_thread_list_page() { - assert(current_plugin->is_image_board()); - ImageBoard *image_board = static_cast(current_plugin); - if(image_board->get_threads(image_board_thread_list_url, body->items) != PluginResult::OK) { - show_notification("Content list", "Failed to get threads for url: " + image_board_thread_list_url, Urgency::CRITICAL); - current_page = Page::SEARCH_SUGGESTION; - return; - } - - search_bar->onTextUpdateCallback = [this](const std::string &text) { - body->filter_search_fuzzy(text); - body->select_first_item(); - }; - - search_bar->onTextSubmitCallback = [this](const std::string&) -> bool { - BodyItem *selected_item = body->get_selected(); - if(!selected_item) - return false; - - content_episode = selected_item->get_title(); - content_url = selected_item->url; - current_page = Page::IMAGE_BOARD_THREAD; - body->clear_items(); - return true; - }; - - sf::Vector2f body_pos; - sf::Vector2f body_size; - bool redraw = true; - sf::Event event; - - while (current_page == Page::IMAGE_BOARD_THREAD_LIST) { - while (window.pollEvent(event)) { - base_event_handler(event, Page::SEARCH_SUGGESTION); - if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) - redraw = true; - } - - if(redraw) { - redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); - } - - search_bar->update(); - - window.clear(back_color); - search_bar->draw(window); - body->draw(window, body_pos, body_size); - window.display(); - } - } - static bool is_url_video(const std::string &url) { return string_ends_with(url, ".webm") || string_ends_with(url, ".mp4") || string_ends_with(url, ".gif"); } - void Program::image_board_thread_page() { - assert(current_plugin->is_image_board()); - // TODO: Support image board other than 4chan. To make this work, the captcha code needs to be changed - // to work with other captcha than google captcha - assert(current_plugin->name == "4chan"); - ImageBoard *image_board = static_cast(current_plugin); - if(image_board->get_thread_comments(image_board_thread_list_url, content_url, body->items) != PluginResult::OK) { - show_notification("Content details", "Failed to get content details for url: " + content_url, Urgency::CRITICAL); - // TODO: This will return to an empty content list. - // Each page should have its own @Body so we can return to the last page and still have the data loaded - // however the cached images should be cleared. - current_page = Page::IMAGE_BOARD_THREAD_LIST; - return; - } - - const std::string &board = image_board_thread_list_url; - const std::string &thread = content_url; - + void Program::image_board_thread_page(ImageBoardThreadPage *thread_page, Body *thread_body) { // TODO: Instead of using stage here, use different pages for each stage enum class NavigationStage { VIEWING_COMMENTS, @@ -2857,7 +2307,7 @@ namespace QuickMedia { // TODO: Make this work with other sites than 4chan auto request_google_captcha_image = [this, &captcha_texture, &captcha_image_mutex, &navigation_stage, &captcha_sprite, &challenge_description_text](GoogleCaptchaChallengeInfo &challenge_info) { std::string payload_image_data; - DownloadResult download_image_result = download_to_string(challenge_info.payload_url, payload_image_data, {}, current_plugin->use_tor); + DownloadResult download_image_result = download_to_string(challenge_info.payload_url, payload_image_data, {}, is_tor_enabled()); if(download_image_result == DownloadResult::OK) { std::lock_guard lock(captcha_image_mutex); if(captcha_texture.loadFromMemory(payload_image_data.data(), payload_image_data.size())) { @@ -2893,40 +2343,40 @@ namespace QuickMedia { show_notification("Google captcha", "Failed to get captcha challenge", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } - }, current_plugin->use_tor); + }, is_tor_enabled()); }; Entry comment_input("Press ctrl+m to begin writing a comment...", font.get(), cjk_font.get()); comment_input.draw_background = false; comment_input.set_editable(false); - auto post_comment = [this, &comment_input, &navigation_stage, &image_board, &board, &thread, &captcha_post_id, &comment_to_post, &request_new_google_captcha_challenge]() { + auto post_comment = [this, &comment_input, &navigation_stage, &thread_page, &captcha_post_id, &comment_to_post, &request_new_google_captcha_challenge]() { comment_input.set_editable(false); navigation_stage = NavigationStage::POSTING_COMMENT; - PostResult post_result = image_board->post_comment(board, thread, captcha_post_id, comment_to_post); + PostResult post_result = thread_page->post_comment(captcha_post_id, comment_to_post); if(post_result == PostResult::OK) { - show_notification(current_plugin->name, "Comment posted!"); + show_notification("QuickMedia", "Comment posted!"); navigation_stage = NavigationStage::VIEWING_COMMENTS; // TODO: Append posted comment to the thread so the user can see their posted comment. // TODO: Asynchronously update the thread periodically to show new comments. } else if(post_result == PostResult::TRY_AGAIN) { - show_notification(current_plugin->name, "Error while posting, did the captcha expire? Please try again"); + show_notification("QuickMedia", "Error while posting, did the captcha expire? Please try again"); // TODO: Check if the response contains a new captcha instead of requesting a new one manually request_new_google_captcha_challenge(); } else if(post_result == PostResult::BANNED) { - show_notification(current_plugin->name, "Failed to post comment because you are banned", Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to post comment because you are banned", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else if(post_result == PostResult::ERR) { - show_notification(current_plugin->name, "Failed to post comment. Is " + current_plugin->name + " down or is your internet down?", Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to post comment. Is " + std::string(plugin_name) + " down or is your internet down?", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } else { assert(false && "Unhandled post result"); - show_notification(current_plugin->name, "Failed to post comment. Unknown error", Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to post comment. Unknown error", Urgency::CRITICAL); navigation_stage = NavigationStage::VIEWING_COMMENTS; } }; - comment_input.on_submit_callback = [&post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment, &image_board](const std::string &text) -> bool { + comment_input.on_submit_callback = [&post_comment_future, &navigation_stage, &request_new_google_captcha_challenge, &comment_to_post, &captcha_post_id, &captcha_solved_time, &post_comment, &thread_page](const std::string &text) -> bool { if(text.empty()) return false; @@ -2937,9 +2387,9 @@ namespace QuickMedia { post_comment(); return true; }); - } else if(image_board->get_pass_id().empty()) { + } else if(thread_page->get_pass_id().empty()) { request_new_google_captcha_challenge(); - } else if(!image_board->get_pass_id().empty()) { + } else if(!thread_page->get_pass_id().empty()) { post_comment_future = std::async(std::launch::async, [&post_comment]() -> bool { post_comment(); return true; @@ -2967,7 +2417,7 @@ namespace QuickMedia { std::stack comment_navigation_stack; std::stack comment_page_scroll_stack; - while (current_page == Page::IMAGE_BOARD_THREAD) { + while (current_page == PageType::IMAGE_BOARD_THREAD) { while (window.pollEvent(event)) { if(navigation_stage == NavigationStage::REPLYING) { comment_input.process_event(event); @@ -2981,8 +2431,9 @@ namespace QuickMedia { } } - if (event.type == sf::Event::Closed) { - current_page = Page::EXIT; + if(event.type == sf::Event::Closed) { + current_page = PageType::EXIT; + window.close(); } else if(event.type == sf::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; @@ -2994,46 +2445,42 @@ namespace QuickMedia { redraw = true; else if(event.type == sf::Event::KeyPressed && navigation_stage == NavigationStage::VIEWING_COMMENTS) { if(event.key.code == sf::Keyboard::Up) { - body->select_previous_item(); + thread_body->select_previous_item(); } else if(event.key.code == sf::Keyboard::Down) { - body->select_next_item(); + thread_body->select_next_item(); } else if(event.key.code == sf::Keyboard::PageUp) { - body->select_previous_page(); + thread_body->select_previous_page(); } else if(event.key.code == sf::Keyboard::PageDown) { - body->select_next_page(); + thread_body->select_next_page(); } else if(event.key.code == sf::Keyboard::Home) { - body->select_first_item(); + thread_body->select_first_item(); } else if(event.key.code == sf::Keyboard::End) { - body->select_last_item(); + thread_body->select_last_item(); } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::IMAGE_BOARD_THREAD_LIST; - body->clear_items(); - body->reset_selected(); + current_page = pop_page_stack(); } else if(event.key.code == sf::Keyboard::P) { - BodyItem *selected_item = body->get_selected(); + BodyItem *selected_item = thread_body->get_selected(); if(selected_item && !selected_item->attached_content_url.empty()) { if(is_url_video(selected_item->attached_content_url)) { - page_stack.push(Page::IMAGE_BOARD_THREAD); - current_page = Page::VIDEO_CONTENT; - std::string prev_content_url = content_url; - content_url = selected_item->attached_content_url; + page_stack.push(PageType::IMAGE_BOARD_THREAD); + current_page = PageType::VIDEO_CONTENT; watched_videos.clear(); - video_content_page(); - content_url = std::move(prev_content_url); + // TODO: Use real title + video_content_page(thread_page, selected_item->attached_content_url, "No title.webm"); redraw = true; } else { if(downloading_image && load_image_future.valid()) load_image_future.get(); downloading_image = true; navigation_stage = NavigationStage::VIEWING_ATTACHED_IMAGE; - load_image_future = std::async(std::launch::async, [this, &image_board]() { + load_image_future = std::async(std::launch::async, [this, &thread_body]() { std::string image_data; - BodyItem *selected_item = body->get_selected(); + BodyItem *selected_item = thread_body->get_selected(); if(!selected_item || selected_item->attached_content_url.empty()) { return image_data; } - if(download_to_string_cache(selected_item->attached_content_url, image_data, {}, image_board->use_tor) != DownloadResult::OK) { - show_notification(image_board->name, "Failed to download image: " + selected_item->attached_content_url, Urgency::CRITICAL); + if(download_to_string_cache(selected_item->attached_content_url, image_data, {}, is_tor_enabled()) != DownloadResult::OK) { + show_notification("QuickMedia", "Failed to download image: " + selected_item->attached_content_url, Urgency::CRITICAL); image_data.clear(); } return image_data; @@ -3042,43 +2489,43 @@ namespace QuickMedia { } } - BodyItem *selected_item = body->get_selected(); - if(event.key.code == sf::Keyboard::Enter && selected_item && (comment_navigation_stack.empty() || body->get_selected_item() != comment_navigation_stack.top()) && !selected_item->replies.empty()) { - for(auto &body_item : body->items) { + BodyItem *selected_item = thread_body->get_selected(); + if(event.key.code == sf::Keyboard::Enter && selected_item && (comment_navigation_stack.empty() || thread_body->get_selected_item() != comment_navigation_stack.top()) && !selected_item->replies.empty()) { + for(auto &body_item : thread_body->items) { body_item->visible = false; } selected_item->visible = true; for(size_t reply_index : selected_item->replies) { - body->items[reply_index]->visible = true; + thread_body->items[reply_index]->visible = true; } - comment_navigation_stack.push(body->get_selected_item()); - comment_page_scroll_stack.push(body->get_page_scroll()); - body->clamp_selection(); - body->set_page_scroll(0.0f); + comment_navigation_stack.push(thread_body->get_selected_item()); + comment_page_scroll_stack.push(thread_body->get_page_scroll()); + thread_body->clamp_selection(); + thread_body->set_page_scroll(0.0f); } else if(event.key.code == sf::Keyboard::BackSpace && !comment_navigation_stack.empty()) { size_t previous_selected = comment_navigation_stack.top(); float previous_page_scroll = comment_page_scroll_stack.top(); comment_navigation_stack.pop(); comment_page_scroll_stack.pop(); if(comment_navigation_stack.empty()) { - for(auto &body_item : body->items) { + for(auto &body_item : thread_body->items) { body_item->visible = true; } - body->set_selected_item(previous_selected); - body->clamp_selection(); + thread_body->set_selected_item(previous_selected); + thread_body->clamp_selection(); } else { - for(auto &body_item : body->items) { + for(auto &body_item : thread_body->items) { body_item->visible = false; } - body->set_selected_item(previous_selected); - selected_item = body->items[comment_navigation_stack.top()].get(); + thread_body->set_selected_item(previous_selected); + selected_item = thread_body->items[comment_navigation_stack.top()].get(); selected_item->visible = true; for(size_t reply_index : selected_item->replies) { - body->items[reply_index]->visible = true; + thread_body->items[reply_index]->visible = true; } - body->clamp_selection(); + thread_body->clamp_selection(); } - body->set_page_scroll(previous_page_scroll); + thread_body->set_page_scroll(previous_page_scroll); } else if(event.key.code == sf::Keyboard::M && event.key.control && selected_item) { navigation_stage = NavigationStage::REPLYING; comment_input.set_editable(true); @@ -3126,7 +2573,7 @@ namespace QuickMedia { } request_google_captcha_image(challenge_info); } - }, current_plugin->use_tor); + }, is_tor_enabled()); } } @@ -3226,11 +2673,11 @@ namespace QuickMedia { //attached_image_texture->generateMipmap(); attached_image_sprite.setTexture(*attached_image_texture, true); } else { - BodyItem *selected_item = body->get_selected(); + BodyItem *selected_item = thread_body->get_selected(); std::string selected_item_attached_url; if(selected_item) selected_item_attached_url = selected_item->attached_content_url; - show_notification(image_board->name, "Failed to load image downloaded from url: " + selected_item->attached_content_url, Urgency::CRITICAL); + show_notification("QuickMedia", "Failed to load image downloaded from url: " + selected_item->attached_content_url, Urgency::CRITICAL); } } @@ -3260,12 +2707,12 @@ namespace QuickMedia { window.draw(comment_input_shade); window.draw(logo_sprite); comment_input.draw(window); - body->draw(window, body_pos, body_size); + thread_body->draw(window, body_pos, body_size); } else if(navigation_stage == NavigationStage::VIEWING_COMMENTS) { window.draw(comment_input_shade); window.draw(logo_sprite); comment_input.draw(window); - body->draw(window, body_pos, body_size); + thread_body->draw(window, body_pos, body_size); } window.display(); } @@ -3279,14 +2726,10 @@ namespace QuickMedia { post_comment_future.get(); if(load_image_future.valid()) load_image_future.get(); - - // Clear post that is still being written. - // TODO: This post should be saved for the thread. Each thread should have its own text edit widget, - // so you dont have to retype a post that was in the middle of being posted when returning. } void Program::chat_login_page() { - assert(current_plugin->name == "matrix"); + assert(strcmp(plugin_name, "matrix") == 0); SearchBar login_input(*font, nullptr, "Username"); SearchBar password_input(*font, nullptr, "Password", true); @@ -3298,23 +2741,22 @@ namespace QuickMedia { SearchBar *inputs[num_inputs] = { &login_input, &password_input, &homeserver_input }; int focused_input = 0; - auto text_submit_callback = [this, inputs, &status_text](const sf::String&) -> bool { - Matrix *matrix = static_cast(current_plugin); + auto text_submit_callback = [this, inputs, &status_text](const sf::String&) { for(int i = 0; i < num_inputs; ++i) { if(inputs[i]->get_text().empty()) { status_text.setString("All fields need to be filled in"); - return false; + return; } } std::string err_msg; // TODO: Make asynchronous if(matrix->login(inputs[0]->get_text(), inputs[1]->get_text(), inputs[2]->get_text(), err_msg) == PluginResult::OK) { - current_page = Page::CHAT; + current_page = PageType::CHAT; } else { status_text.setString("Failed to login, error: " + err_msg); } - return false; + return; }; for(int i = 0; i < num_inputs; ++i) { @@ -3328,9 +2770,11 @@ namespace QuickMedia { bool redraw = true; sf::Event event; - while (current_page == Page::CHAT_LOGIN) { + auto body = create_body(); + + while (current_page == PageType::CHAT_LOGIN) { while (window.pollEvent(event)) { - base_event_handler(event, Page::EXIT, false, false, false); + base_event_handler(event, PageType::EXIT, body.get(), nullptr, false, false); if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) { redraw = true; } else if(event.type == sf::Event::TextEntered) { @@ -3347,8 +2791,7 @@ namespace QuickMedia { if(redraw) { redraw = false; - search_bar->onWindowResize(window_size); - get_body_dimensions(window_size, search_bar.get(), body_pos, body_size); + get_body_dimensions(window_size, nullptr, body_pos, body_size); } window.clear(back_color); @@ -3392,8 +2835,9 @@ namespace QuickMedia { } void Program::chat_page() { - assert(current_plugin->name == "matrix"); - Matrix *matrix = static_cast(current_plugin); + assert(strcmp(plugin_name, "matrix") == 0); + + auto video_page = std::make_unique(this); std::vector tabs; int selected_tab = 0; @@ -3439,7 +2883,7 @@ namespace QuickMedia { bool is_window_focused = window.hasFocus(); - auto process_new_room_messages = [matrix, &body_items_by_room_id, ¤t_room_id, &is_window_focused](RoomSyncMessages &room_sync_messages, bool only_show_mentions) mutable { + auto process_new_room_messages = [this, &body_items_by_room_id, ¤t_room_id, &is_window_focused](RoomSyncMessages &room_sync_messages, bool only_show_mentions) mutable { for(auto &[room, messages] : room_sync_messages) { bool was_mentioned = false; for(auto &message : messages) { @@ -3494,7 +2938,7 @@ namespace QuickMedia { URL_SELECTION }; - Page new_page = Page::CHAT; + PageType new_page = PageType::CHAT; ChatState chat_state = ChatState::NAVIGATING; std::shared_ptr currently_operating_on_item; @@ -3506,7 +2950,7 @@ namespace QuickMedia { chat_input.draw_background = false; chat_input.set_editable(false); - chat_input.on_submit_callback = [matrix, &chat_input, &tabs, &selected_tab, ¤t_room_id, &new_page, &chat_state, ¤tly_operating_on_item](const std::string &text) mutable { + chat_input.on_submit_callback = [this, &chat_input, &tabs, &selected_tab, ¤t_room_id, &new_page, &chat_state, ¤tly_operating_on_item](const std::string &text) mutable { if(tabs[selected_tab].type == ChatTabType::MESSAGES) { if(text.empty()) return false; @@ -3514,12 +2958,12 @@ namespace QuickMedia { if(chat_state == ChatState::TYPING_MESSAGE && text[0] == '/') { std::string command = strip(text); if(command == "/upload") { - new_page = Page::FILE_MANAGER; + new_page = PageType::FILE_MANAGER; chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; } else if(command == "/logout") { - new_page = Page::CHAT_LOGIN; + new_page = PageType::CHAT_LOGIN; chat_input.set_editable(false); chat_state = ChatState::NAVIGATING; return true; @@ -3622,7 +3066,7 @@ namespace QuickMedia { auto room_avatar_thumbnail_data = std::make_shared(); AsyncImageLoader async_image_loader; - auto typing_async_func = [matrix](bool new_state, std::string room_id) { + auto typing_async_func = [this](bool new_state, std::string room_id) { if(new_state) { matrix->on_start_typing(room_id); } else { @@ -3650,17 +3094,17 @@ namespace QuickMedia { std::future set_read_marker_future; bool setting_read_marker = false; - auto launch_url = [this, &redraw](const std::string &url) mutable { + auto launch_url = [this, &video_page, &redraw](const std::string &url) mutable { if(url.empty()) return; std::string video_id; if(youtube_url_extract_id(url, video_id)) { - page_stack.push(Page::CHAT); + page_stack.push(PageType::CHAT); watched_videos.clear(); - content_url = url; - current_page = Page::VIDEO_CONTENT; - video_content_page(); + current_page = PageType::VIDEO_CONTENT; + // TODO: Add title + video_content_page(video_page.get(), url, "No title"); redraw = true; } else { if(!is_program_executable_by_name("xdg-open")) { @@ -3710,10 +3154,10 @@ namespace QuickMedia { return result; }; - while (current_page == Page::CHAT) { + while (current_page == PageType::CHAT) { sf::Int32 frame_time_ms = frame_timer.restart().asMilliseconds(); while (window.pollEvent(event)) { - base_event_handler(event, Page::EXIT, false, false, false); + base_event_handler(event, PageType::EXIT, tabs[selected_tab].body.get(), nullptr, false, false); if(event.type == sf::Event::GainedFocus) { is_window_focused = true; redraw = true; @@ -3744,7 +3188,6 @@ namespace QuickMedia { fetching_previous_messages_running = true; previous_messages_future_room_id = current_room_id; previous_messages_future = std::async(std::launch::async, [this, &previous_messages_future_room_id]() { - Matrix *matrix = static_cast(current_plugin); BodyItems result_items; if(matrix->get_previous_room_messages(previous_messages_future_room_id, result_items) != PluginResult::OK) fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", previous_messages_future_room_id.c_str()); @@ -3758,9 +3201,7 @@ namespace QuickMedia { } else if(event.key.code == sf::Keyboard::End) { tabs[selected_tab].body->select_last_item(); } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::EXIT; - body->clear_items(); - body->reset_selected(); + current_page = PageType::EXIT; } else if(event.key.code == sf::Keyboard::Left && synced) { tabs[selected_tab].body->clear_thumbnails(); selected_tab = std::max(0, selected_tab - 1); @@ -3836,11 +3277,11 @@ namespace QuickMedia { if(!selected->url.empty()) { const char *content_type = link_get_content_type(selected->url); if(content_type && (strcmp(content_type, "audio") == 0 || strcmp(content_type, "video") == 0 || strcmp(content_type, "image") == 0)) { - page_stack.push(Page::CHAT); + page_stack.push(PageType::CHAT); watched_videos.clear(); - content_url = selected->url; - current_page = Page::VIDEO_CONTENT; - video_content_page(); + current_page = PageType::VIDEO_CONTENT; + // TODO: Add title + video_content_page(video_page.get(), selected->url, "No title"); redraw = true; continue; } @@ -3959,15 +3400,19 @@ namespace QuickMedia { } switch(new_page) { - case Page::FILE_MANAGER: { - new_page = Page::CHAT; - if(!file_manager) { - file_manager = new FileManager(); - file_manager->set_current_directory(get_home_dir().data); - } - page_stack.push(Page::CHAT); - current_page = Page::FILE_MANAGER; - file_manager_page(); + case PageType::FILE_MANAGER: { + new_page = PageType::CHAT; + + auto file_manager_page = std::make_unique(this); + file_manager_page->set_current_directory(get_home_dir().data); + auto file_manager_body = create_body(); + file_manager_page->get_files_in_directory(file_manager_body->items); + std::vector tabs; + tabs.push_back(Tab{std::move(file_manager_body), std::move(file_manager_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + selected_files.clear(); + page_loop(std::move(tabs)); + if(selected_files.empty()) { fprintf(stderr, "No files selected!\n"); } else { @@ -3982,8 +3427,8 @@ namespace QuickMedia { redraw = true; break; } - case Page::CHAT_LOGIN: { - new_page = Page::CHAT; + case PageType::CHAT_LOGIN: { + new_page = PageType::CHAT; matrix->logout(); tabs[MESSAGES_TAB_INDEX].body->clear_thumbnails(); // TODO: Instead of doing this, exit this current function and navigate to chat login page instead. @@ -3991,9 +3436,9 @@ namespace QuickMedia { // and one of them is /sync, which has a timeout of 30 seconds. That timeout has to be killed somehow. //delete current_plugin; //current_plugin = new Matrix(); - current_page = Page::CHAT_LOGIN; + current_page = PageType::CHAT_LOGIN; chat_login_page(); - if(current_page == Page::CHAT) + if(current_page == PageType::CHAT) chat_page(); exit(0); break; @@ -4091,8 +3536,6 @@ namespace QuickMedia { sync_timer.restart(); sync_future_room_id = current_room_id; sync_future = std::async(std::launch::async, [this, &sync_future_room_id, synced]() { - Matrix *matrix = static_cast(current_plugin); - SyncFutureResult result; if(matrix->sync(result.room_sync_messages) == PluginResult::OK) { fprintf(stderr, "Synced matrix\n"); @@ -4100,7 +3543,7 @@ namespace QuickMedia { if(!synced) { if(matrix->get_joined_rooms(result.rooms_body_items) != PluginResult::OK) { show_notification("QuickMedia", "Failed to get a list of joined rooms", Urgency::CRITICAL); - current_page = Page::EXIT; + current_page = PageType::EXIT; return result; } } @@ -4311,7 +3754,7 @@ namespace QuickMedia { current_room_body_data->last_read_message_timestamp = message->timestamp; // TODO: What if the message is no longer valid? setting_read_marker = true; - set_read_marker_future = std::async(std::launch::async, [matrix, current_room_id, message]() mutable { + set_read_marker_future = std::async(std::launch::async, [this, current_room_id, message]() mutable { if(matrix->set_read_marker(current_room_id, message) != PluginResult::OK) { fprintf(stderr, "Warning: failed to set read marker to %s\n", message->event_id.c_str()); } diff --git a/src/SearchBar.cpp b/src/SearchBar.cpp index f489779..382b06a 100644 --- a/src/SearchBar.cpp +++ b/src/SearchBar.cpp @@ -32,6 +32,7 @@ namespace QuickMedia { draw_logo(false), needs_update(true), input_masked(input_masked), + typing(false), vertical_pos(0.0f) { text.setFillColor(text_placeholder_color); @@ -97,6 +98,7 @@ namespace QuickMedia { u8_str->clear(); if(onTextUpdateCallback) onTextUpdateCallback(*u8_str); + typing = false; } else if(updated_autocomplete && elapsed_time >= autocomplete_search_delay) { updated_autocomplete = false; if(!show_placeholder && onAutocompleteRequestCallback) { @@ -160,24 +162,23 @@ namespace QuickMedia { } else { clear_autocomplete_if_text_not_substring(); } - if(!updated_search && onTextBeginTypingCallback) - onTextBeginTypingCallback(); + if(!updated_search) { + typing = true; + if(onTextBeginTypingCallback) + onTextBeginTypingCallback(); + } updated_search = true; updated_autocomplete = true; time_since_search_update.restart(); } } else if(codepoint == 13) { // Return - bool clear_search = true; if(onTextSubmitCallback) { auto u8 = text.getString().toUtf8(); std::string *u8_str = (std::string*)&u8; if(show_placeholder) u8_str->clear(); - clear_search = onTextSubmitCallback(*u8_str); + onTextSubmitCallback(*u8_str); } - - if(clear_search) - clear(); } else if(codepoint > 31) { // Non-control character append_text(sf::String(codepoint)); } else if(codepoint == '\n') @@ -211,8 +212,11 @@ namespace QuickMedia { text.setString(str); clear_autocomplete_if_text_not_substring(); - if(!updated_search && onTextBeginTypingCallback) - onTextBeginTypingCallback(); + if(!updated_search) { + typing = true; + if(onTextBeginTypingCallback) + onTextBeginTypingCallback(); + } updated_search = true; updated_autocomplete = true; time_since_search_update.restart(); @@ -235,8 +239,11 @@ namespace QuickMedia { text.setFillColor(sf::Color::White); } text.setString(autocomplete_str); - if(!updated_search && onTextBeginTypingCallback) - onTextBeginTypingCallback(); + if(!updated_search) { + typing = true; + if(onTextBeginTypingCallback) + onTextBeginTypingCallback(); + } updated_search = true; updated_autocomplete = true; time_since_search_update.restart(); diff --git a/src/Storage.cpp b/src/Storage.cpp index cd34b56..c9dfb17 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -1,9 +1,11 @@ #include "../include/Storage.hpp" #include "../include/env.hpp" +#include "../include/StringUtils.hpp" #include #include #include #include +#include #if OS_FAMILY == OS_FAMILY_POSIX #include @@ -223,4 +225,23 @@ namespace QuickMedia { return true; } + + bool is_program_executable_by_name(const char *name) { + // TODO: Implement for Windows. Windows also uses semicolon instead of colon as a separator + char *env = getenv("PATH"); + std::unordered_set paths; + string_split(env, ':', [&paths](const char *str, size_t size) { + paths.insert(std::string(str, size)); + return true; + }); + + for(const std::string &path_str : paths) { + Path path(path_str); + path.join(name); + if(get_file_type(path) == FileType::REGULAR) + return true; + } + + return false; + } } \ No newline at end of file diff --git a/src/plugins/Dmenu.cpp b/src/plugins/Dmenu.cpp deleted file mode 100644 index 9a8b5b8..0000000 --- a/src/plugins/Dmenu.cpp +++ /dev/null @@ -1,23 +0,0 @@ -#include "../../plugins/Dmenu.hpp" -#include - -namespace QuickMedia { - Dmenu::Dmenu() : Plugin("dmenu") { - std::string line; - while(std::getline(std::cin, line)) { - stdin_data.push_back(std::move(line)); - } - } - - PluginResult Dmenu::get_front_page(BodyItems &result_items) { - for(const std::string &line_data : stdin_data) { - result_items.push_back(BodyItem::create(line_data)); - } - return PluginResult::OK; - } - - SearchResult Dmenu::search(const std::string &text, BodyItems&) { - std::cout << text << std::endl; - return SearchResult::OK; - } -} \ No newline at end of file diff --git a/src/plugins/FileManager.cpp b/src/plugins/FileManager.cpp index ccaf2c1..5fac79c 100644 --- a/src/plugins/FileManager.cpp +++ b/src/plugins/FileManager.cpp @@ -1,12 +1,8 @@ #include "../../plugins/FileManager.hpp" #include "../../include/ImageUtils.hpp" -#include +#include "../../include/QuickMedia.hpp" namespace QuickMedia { - FileManager::FileManager() : Plugin("file-manager"), current_dir("/") { - - } - // Returns empty string if no extension static const char* get_ext(const std::filesystem::path &path) { const char *path_c = path.c_str(); @@ -26,7 +22,45 @@ namespace QuickMedia { } } - PluginResult FileManager::get_files_in_directory(BodyItems &result_items) { + PluginResult FileManagerPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + (void)url; + + std::filesystem::path new_path; + if(title == "..") + new_path = current_dir.parent_path(); + else + new_path = current_dir / title; + + if(std::filesystem::is_regular_file(new_path)) { + program->select_file(new_path); + return PluginResult::OK; + } + + if(!std::filesystem::is_directory(new_path)) + return PluginResult::ERR; + + current_dir = std::move(new_path); + + BodyItems result_items; + PluginResult result = get_files_in_directory(result_items); + if(result != PluginResult::OK) + return result; + + auto body = create_body(); + body->items = std::move(result_items); + body->draw_thumbnails = true; + result_tabs.push_back(Tab{std::move(body), nullptr, nullptr}); + return PluginResult::OK; + } + + bool FileManagerPage::set_current_directory(const std::string &path) { + if(!std::filesystem::is_directory(path)) + return false; + current_dir = path; + return true; + } + + PluginResult FileManagerPage::get_files_in_directory(BodyItems &result_items) { std::vector paths; try { for(auto &p : std::filesystem::directory_iterator(current_dir)) { @@ -58,33 +92,4 @@ namespace QuickMedia { return PluginResult::OK; } - - bool FileManager::set_current_directory(const std::string &path) { - if(!std::filesystem::is_directory(path)) - return false; - current_dir = path; - return true; - } - - bool FileManager::set_child_directory(const std::string &filename) { - if(filename == "..") { - std::filesystem::path new_path = current_dir.parent_path(); - if(std::filesystem::is_directory(new_path)) { - current_dir = std::move(new_path); - return true; - } - return false; - } else { - std::filesystem::path new_path = current_dir / filename; - if(std::filesystem::is_directory(new_path)) { - current_dir = std::move(new_path); - return true; - } - return false; - } - } - - const std::filesystem::path& FileManager::get_current_dir() const { - return current_dir; - } } \ No newline at end of file diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp index 1cecc2b..1d3681a 100644 --- a/src/plugins/Fourchan.cpp +++ b/src/plugins/Fourchan.cpp @@ -1,6 +1,7 @@ #include "../../plugins/Fourchan.hpp" #include "../../include/DataView.hpp" #include "../../include/Storage.hpp" +#include "../../include/StringUtils.hpp" #include #include #include @@ -11,40 +12,9 @@ static const std::string fourchan_url = "https://a.4cdn.org/"; static const std::string fourchan_image_url = "https://i.4cdn.org/"; -namespace QuickMedia { - Fourchan::Fourchan(const std::string &resources_root) : ImageBoard("4chan"), resources_root(resources_root) { - thread_list_update_thread = std::thread([this]() { - BodyItems new_thread_list_items; - while(running) { - new_thread_list_items.clear(); - auto start_time = std::chrono::steady_clock::now(); - - std::string board_url = get_board_url(); - if(!board_url.empty()) { - PluginResult plugin_result = get_threads_internal(board_url, new_thread_list_items); - if(plugin_result == PluginResult::OK) - set_board_thread_list(std::move(new_thread_list_items)); - } - - auto time_passed = std::chrono::steady_clock::now() - start_time; - if(time_passed < std::chrono::seconds(15)) { - auto time_to_sleep = std::chrono::seconds(15) - time_passed; - std::unique_lock lock(thread_list_cache_mutex); - thread_list_update_cv.wait_for(lock, time_to_sleep); - } - } - }); - } - - Fourchan::~Fourchan() { - running = false; - { - std::unique_lock lock(thread_list_cache_mutex); - thread_list_update_cv.notify_one(); - } - thread_list_update_thread.join(); - } +static const char *SERVICE_NAME = "4chan"; +namespace QuickMedia { // Returns empty string on failure to read cookie static std::string get_pass_id_from_cookies_file(const Path &cookies_filepath) { std::string file_content; @@ -63,53 +33,6 @@ namespace QuickMedia { return strip(file_content.substr(pass_id_index, line_end - pass_id_index)); } - PluginResult Fourchan::get_front_page(BodyItems &result_items) { - if(pass_id.empty()) { - Path cookies_filepath; - if(get_cookies_filepath(cookies_filepath, name) != 0) { - fprintf(stderr, "Failed to get 4chan cookies filepath\n"); - } else { - pass_id = get_pass_id_from_cookies_file(cookies_filepath); - } - } - - std::string server_response; - if(file_get_content(resources_root + "boards.json", server_response) != 0) { - fprintf(stderr, "failed to read boards.json\n"); - return PluginResult::ERR; - } - - Json::Value json_root; - Json::CharReaderBuilder json_builder; - std::unique_ptr json_reader(json_builder.newCharReader()); - std::string json_errors; - if(!json_reader->parse(server_response.data(), server_response.data() + server_response.size(), &json_root, &json_errors)) { - fprintf(stderr, "4chan front page json error: %s\n", json_errors.c_str()); - return PluginResult::ERR; - } - - if(!json_root.isObject()) - return PluginResult::ERR; - - const Json::Value &boards = json_root["boards"]; - if(boards.isArray()) { - for(const Json::Value &board : boards) { - const Json::Value &board_id = board["board"]; // /g/, /a/, /b/ etc - const Json::Value &board_title = board["title"]; - const Json::Value &board_description = board["meta_description"]; - if(board_id.isString() && board_title.isString() && board_description.isString()) { - std::string board_description_str = board_description.asString(); - html_unescape_sequences(board_description_str); - auto body_item = BodyItem::create("/" + board_id.asString() + "/ " + board_title.asString()); - body_item->url = board_id.asString(); - result_items.emplace_back(std::move(body_item)); - } - } - } - - return PluginResult::OK; - } - struct CommentPiece { enum class Type { TEXT, @@ -210,270 +133,74 @@ namespace QuickMedia { tidyRelease(doc); } - PluginResult Fourchan::get_threads_internal(const std::string &url, BodyItems &result_items) { + PluginResult FourchanBoardsPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { Json::Value json_root; DownloadResult result = download_json(json_root, fourchan_url + url + "/catalog.json", {}, true); if(result != DownloadResult::OK) return download_result_to_plugin_result(result); - if(json_root.isArray()) { - 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; - extract_comment_pieces(sub_begin, sub_end - sub_begin, - [&title_text](const CommentPiece &cp) { - switch(cp.type) { - case CommentPiece::Type::TEXT: - title_text.append(cp.text.data, cp.text.size); - break; - case CommentPiece::Type::QUOTE: - title_text += '>'; - title_text.append(cp.text.data, cp.text.size); - //comment_text += '\n'; - break; - case CommentPiece::Type::QUOTELINK: { - title_text.append(cp.text.data, cp.text.size); - break; - } - case CommentPiece::Type::LINE_CONTINUE: { - if(!title_text.empty() && title_text.back() == '\n') { - title_text.pop_back(); - } - break; - } - } - } - ); - if(!title_text.empty() && title_text.back() == '\n') - title_text.back() = ' '; - html_unescape_sequences(title_text); - - std::string comment_text; - extract_comment_pieces(comment_begin, comment_end - comment_begin, - [&comment_text](const CommentPiece &cp) { - switch(cp.type) { - case CommentPiece::Type::TEXT: - comment_text.append(cp.text.data, cp.text.size); - break; - case CommentPiece::Type::QUOTE: - comment_text += '>'; - comment_text.append(cp.text.data, cp.text.size); - //comment_text += '\n'; - break; - case CommentPiece::Type::QUOTELINK: { - comment_text.append(cp.text.data, cp.text.size); - break; - } - case CommentPiece::Type::LINE_CONTINUE: { - if(!comment_text.empty() && comment_text.back() == '\n') { - comment_text.pop_back(); - } - break; - } - } - } - ); - html_unescape_sequences(comment_text); - // TODO: Do the same when wrapping is implemented - // TODO: Remove this - int num_lines = 0; - for(size_t i = 0; i < comment_text.size(); ++i) { - if(comment_text[i] == '\n') { - ++num_lines; - if(num_lines == 6) { - comment_text = comment_text.substr(0, i) + " (...)"; - break; - } - } - } - auto body_item = BodyItem::create(std::move(comment_text)); - 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 + url + "/" + std::to_string(tim.asInt64()) + "s.jpg"; - } - - result_items.emplace_back(std::move(body_item)); - } - } - } - - return PluginResult::OK; - } - - void Fourchan::set_board_url(const std::string &new_url) { - { - std::lock_guard lock(board_url_mutex); - if(current_board_url == new_url) - return; - current_board_url = new_url; - } - - std::lock_guard thread_list_lock(thread_list_cache_mutex); - thread_list_update_cv.notify_one(); - thread_list_cached = false; - } - - std::string Fourchan::get_board_url() { - std::lock_guard lock(board_url_mutex); - return current_board_url; - } - - void Fourchan::set_board_thread_list(BodyItems body_items) { - { - std::lock_guard lock(board_list_mutex); - cached_thread_list_items.clear(); - for(auto &body_item : body_items) { - cached_thread_list_items.push_back(std::move(body_item)); - } - } - - std::unique_lock thread_list_cache_lock(thread_list_cache_mutex); - if(!thread_list_cached) { - thread_list_cached = true; - thread_list_cached_cv.notify_one(); - } - } - - BodyItems Fourchan::get_board_thread_list() { - std::lock_guard lock(board_list_mutex); - BodyItems body_items; - for(auto &cached_body_item : cached_thread_list_items) { - body_items.push_back(std::make_shared(*cached_body_item)); - } - return body_items; - } - - PluginResult Fourchan::get_threads(const std::string &url, BodyItems &result_items) { - set_board_url(url); - - std::unique_lock lock(thread_list_cache_mutex); - if(!thread_list_cached) { - if(thread_list_cached_cv.wait_for(lock, std::chrono::seconds(10)) == std::cv_status::timeout) - return PluginResult::NET_ERR; - } - - result_items = get_board_thread_list(); - return PluginResult::OK; - } - - // TODO: Merge with get_threads_internal - PluginResult Fourchan::get_thread_comments(const std::string &list_url, const std::string &url, BodyItems &result_items) { - cached_media_urls.clear(); - - Json::Value json_root; - DownloadResult result = download_json(json_root, fourchan_url + list_url + "/thread/" + url + ".json", {}, true); - if(result != DownloadResult::OK) return download_result_to_plugin_result(result); - - if(!json_root.isObject()) + if(!json_root.isArray()) return PluginResult::ERR; - std::unordered_map comment_by_postno; + BodyItems result_items; - const Json::Value &posts = json_root["posts"]; - if(posts.isArray()) { - for(const Json::Value &post : posts) { - if(!post.isObject()) - continue; + for(const Json::Value &page_data : json_root) { + if(!page_data.isObject()) + continue; - const Json::Value &post_num = post["no"]; - if(!post_num.isNumeric()) - continue; - - int64_t post_num_int = post_num.asInt64(); - comment_by_postno[post_num_int] = result_items.size(); - result_items.push_back(BodyItem::create("")); - result_items.back()->post_number = std::to_string(post_num_int); - } - } + const Json::Value &threads = page_data["threads"]; + if(!threads.isArray()) + continue; - size_t body_item_index = 0; - if(posts.isArray()) { - for(const Json::Value &post : posts) { - if(!post.isObject()) + for(const Json::Value &thread : threads) { + if(!thread.isObject()) continue; - const Json::Value &sub = post["sub"]; + 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 = post["com"]; + 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 &post_num = post["no"]; - if(!post_num.isNumeric()) + const Json::Value &thread_num = thread["no"]; + if(!thread_num.isNumeric()) continue; - const Json::Value &author = post["name"]; - std::string author_str = "Anonymous"; - if(author.isString()) - author_str = author.asString(); - - std::string comment_text; + std::string title_text; extract_comment_pieces(sub_begin, sub_end - sub_begin, - [&comment_text](const CommentPiece &cp) { + [&title_text](const CommentPiece &cp) { switch(cp.type) { case CommentPiece::Type::TEXT: - comment_text.append(cp.text.data, cp.text.size); + title_text.append(cp.text.data, cp.text.size); break; case CommentPiece::Type::QUOTE: - comment_text += '>'; - comment_text.append(cp.text.data, cp.text.size); + title_text += '>'; + title_text.append(cp.text.data, cp.text.size); //comment_text += '\n'; break; case CommentPiece::Type::QUOTELINK: { - comment_text.append(cp.text.data, cp.text.size); + title_text.append(cp.text.data, cp.text.size); break; } case CommentPiece::Type::LINE_CONTINUE: { - if(!comment_text.empty() && comment_text.back() == '\n') { - comment_text.pop_back(); + if(!title_text.empty() && title_text.back() == '\n') { + title_text.pop_back(); } break; } } } ); - if(!comment_text.empty()) - comment_text += '\n'; + if(!title_text.empty() && title_text.back() == '\n') + title_text.back() = ' '; + html_unescape_sequences(title_text); + + std::string comment_text; extract_comment_pieces(comment_begin, comment_end - comment_begin, - [&comment_text, &comment_by_postno, &result_items, body_item_index](const CommentPiece &cp) { + [&comment_text](const CommentPiece &cp) { switch(cp.type) { case CommentPiece::Type::TEXT: comment_text.append(cp.text.data, cp.text.size); @@ -485,13 +212,6 @@ namespace QuickMedia { break; case CommentPiece::Type::QUOTELINK: { comment_text.append(cp.text.data, cp.text.size); - auto it = comment_by_postno.find(cp.quote_postnumber); - if(it == comment_by_postno.end()) { - // TODO: Link this quote to a 4chan archive that still has the quoted comment (if available) - comment_text += "(dead)"; - } else { - result_items[it->second]->replies.push_back(body_item_index); - } break; } case CommentPiece::Type::LINE_CONTINUE: { @@ -503,15 +223,25 @@ namespace QuickMedia { } } ); - if(!comment_text.empty() && comment_text.back() == '\n') - comment_text.back() = ' '; html_unescape_sequences(comment_text); - BodyItem *body_item = result_items[body_item_index].get(); - body_item->set_title(std::move(comment_text)); - body_item->set_author(std::move(author_str)); + // TODO: Do the same when wrapping is implemented + // TODO: Remove this + int num_lines = 0; + for(size_t i = 0; i < comment_text.size(); ++i) { + if(comment_text[i] == '\n') { + ++num_lines; + if(num_lines == 6) { + comment_text = comment_text.substr(0, i) + " (...)"; + break; + } + } + } + auto body_item = BodyItem::create(std::move(comment_text)); + body_item->set_author(std::move(title_text)); + body_item->url = std::to_string(thread_num.asInt64()); - const Json::Value &ext = post["ext"]; - const Json::Value &tim = post["tim"]; + 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") { @@ -520,76 +250,210 @@ namespace QuickMedia { } // "s" means small, that's the url 4chan uses for thumbnails. // thumbnails always has .jpg extension even if they are gifs or webm. - std::string tim_str = std::to_string(tim.asInt64()); - body_item->thumbnail_url = fourchan_image_url + list_url + "/" + tim_str + "s.jpg"; - body_item->attached_content_url = fourchan_image_url + list_url + "/" + tim_str + ext_str; - cached_media_urls.push_back(body_item->attached_content_url); + body_item->thumbnail_url = fourchan_image_url + url + "/" + std::to_string(tim.asInt64()) + "s.jpg"; } - ++body_item_index; + result_items.push_back(std::move(body_item)); } } + auto body = create_body(); + body->items = std::move(result_items); + body->draw_thumbnails = true; + result_tabs.push_back(Tab{std::move(body), std::make_unique(program, title, url), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); return PluginResult::OK; } - PostResult Fourchan::post_comment(const std::string &board, const std::string &thread, const std::string &captcha_id, const std::string &comment) { - std::string url = "https://sys.4chan.org/" + board + "/post"; + void FourchanBoardsPage::get_boards(BodyItems &result_items) { + std::string server_response; + if(file_get_content(resources_root + "boards.json", server_response) != 0) { + fprintf(stderr, "failed to read boards.json\n"); + return; + } - std::vector additional_args = { - CommandArg{"-H", "Referer: https://boards.4chan.org/"}, - CommandArg{"-H", "Origin: https://boards.4chan.org"}, - CommandArg{"-F", "resto=" + thread}, - CommandArg{"-F", "com=" + comment}, - CommandArg{"-F", "mode=regist"} - }; + Json::Value json_root; + Json::CharReaderBuilder json_builder; + std::unique_ptr json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(server_response.data(), server_response.data() + server_response.size(), &json_root, &json_errors)) { + fprintf(stderr, "4chan front page json error: %s\n", json_errors.c_str()); + return; + } - if(pass_id.empty()) { - additional_args.push_back(CommandArg{"-F", "g-recaptcha-response=" + captcha_id}); - } else { - Path cookies_filepath; - if(get_cookies_filepath(cookies_filepath, name) != 0) { - fprintf(stderr, "Failed to get 4chan cookies filepath\n"); - return PostResult::ERR; - } else { - additional_args.push_back(CommandArg{"-c", cookies_filepath.data}); - additional_args.push_back(CommandArg{"-b", cookies_filepath.data}); + if(!json_root.isObject()) + return; + + const Json::Value &boards = json_root["boards"]; + if(!boards.isArray()) + return; + + for(const Json::Value &board : boards) { + const Json::Value &board_id = board["board"]; // /g/, /a/, /b/ etc + const Json::Value &board_title = board["title"]; + const Json::Value &board_description = board["meta_description"]; + if(board_id.isString() && board_title.isString() && board_description.isString()) { + std::string board_description_str = board_description.asString(); + html_unescape_sequences(board_description_str); + auto body_item = BodyItem::create("/" + board_id.asString() + "/ " + board_title.asString()); + body_item->url = board_id.asString(); + result_items.push_back(std::move(body_item)); } } - - std::string response; - if(download_to_string(url, response, additional_args, use_tor, true) != DownloadResult::OK) - return PostResult::ERR; - - if(response.find("successful") != std::string::npos) - return PostResult::OK; - if(response.find("banned") != std::string::npos) - return PostResult::BANNED; - if(response.find("try again") != std::string::npos || response.find("No valid captcha") != std::string::npos) - return PostResult::TRY_AGAIN; - return PostResult::ERR; } - BodyItems Fourchan::get_related_media(const std::string &url) { - BodyItems body_items; - auto it = std::find(cached_media_urls.begin(), cached_media_urls.end(), url); - if(it == cached_media_urls.end()) - return body_items; - - ++it; - for(; it != cached_media_urls.end(); ++it) { - auto body_item = BodyItem::create(""); - body_item->url = *it; - body_items.push_back(std::move(body_item)); + // TODO: Merge with get_threads_internal + PluginResult FourchanThreadListPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + (void)title; + cached_media_urls.clear(); + + Json::Value json_root; + DownloadResult result = download_json(json_root, fourchan_url + board_id + "/thread/" + url + ".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"]; + if(!posts.isArray()) + return PluginResult::OK; + + for(const Json::Value &post : posts) { + if(!post.isObject()) + continue; + + const Json::Value &post_num = post["no"]; + if(!post_num.isNumeric()) + continue; + + int64_t post_num_int = post_num.asInt64(); + comment_by_postno[post_num_int] = result_items.size(); + result_items.push_back(BodyItem::create("")); + result_items.back()->post_number = std::to_string(post_num_int); } - return body_items; + + size_t body_item_index = 0; + for(const Json::Value &post : posts) { + if(!post.isObject()) + continue; + + const Json::Value &sub = post["sub"]; + const char *sub_begin = ""; + const char *sub_end = sub_begin; + sub.getString(&sub_begin, &sub_end); + + const Json::Value &com = post["com"]; + const char *comment_begin = ""; + const char *comment_end = comment_begin; + com.getString(&comment_begin, &comment_end); + + const Json::Value &post_num = post["no"]; + if(!post_num.isNumeric()) + continue; + + const Json::Value &author = post["name"]; + std::string author_str = "Anonymous"; + if(author.isString()) + author_str = author.asString(); + + std::string comment_text; + extract_comment_pieces(sub_begin, sub_end - sub_begin, + [&comment_text](const CommentPiece &cp) { + switch(cp.type) { + case CommentPiece::Type::TEXT: + comment_text.append(cp.text.data, cp.text.size); + break; + case CommentPiece::Type::QUOTE: + comment_text += '>'; + comment_text.append(cp.text.data, cp.text.size); + //comment_text += '\n'; + break; + case CommentPiece::Type::QUOTELINK: { + comment_text.append(cp.text.data, cp.text.size); + break; + } + case CommentPiece::Type::LINE_CONTINUE: { + if(!comment_text.empty() && comment_text.back() == '\n') { + comment_text.pop_back(); + } + break; + } + } + } + ); + if(!comment_text.empty()) + comment_text += '\n'; + extract_comment_pieces(comment_begin, comment_end - comment_begin, + [&comment_text, &comment_by_postno, &result_items, body_item_index](const CommentPiece &cp) { + switch(cp.type) { + case CommentPiece::Type::TEXT: + comment_text.append(cp.text.data, cp.text.size); + break; + case CommentPiece::Type::QUOTE: + comment_text += '>'; + comment_text.append(cp.text.data, cp.text.size); + //comment_text += '\n'; + break; + case CommentPiece::Type::QUOTELINK: { + comment_text.append(cp.text.data, cp.text.size); + auto it = comment_by_postno.find(cp.quote_postnumber); + if(it == comment_by_postno.end()) { + // TODO: Link this quote to a 4chan archive that still has the quoted comment (if available) + comment_text += "(dead)"; + } else { + result_items[it->second]->replies.push_back(body_item_index); + } + break; + } + case CommentPiece::Type::LINE_CONTINUE: { + if(!comment_text.empty() && comment_text.back() == '\n') { + comment_text.pop_back(); + } + break; + } + } + } + ); + if(!comment_text.empty() && comment_text.back() == '\n') + comment_text.back() = ' '; + html_unescape_sequences(comment_text); + BodyItem *body_item = result_items[body_item_index].get(); + body_item->set_title(std::move(comment_text)); + body_item->set_author(std::move(author_str)); + + const Json::Value &ext = post["ext"]; + const Json::Value &tim = post["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. + std::string tim_str = std::to_string(tim.asInt64()); + body_item->thumbnail_url = fourchan_image_url + board_id + "/" + tim_str + "s.jpg"; + body_item->attached_content_url = fourchan_image_url + board_id + "/" + tim_str + ext_str; + cached_media_urls.push_back(body_item->attached_content_url); + } + + ++body_item_index; + } + + auto body = create_body(); + body->items = std::move(result_items); + body->draw_thumbnails = true; + result_tabs.push_back(Tab{std::move(body), std::make_unique(program, board_id, url, std::move(cached_media_urls)), nullptr}); + return PluginResult::OK; } - PluginResult Fourchan::login(const std::string &token, const std::string &pin, std::string &response_msg) { + PluginResult FourchanThreadPage::login(const std::string &token, const std::string &pin, std::string &response_msg) { response_msg.clear(); Path cookies_filepath; - if(get_cookies_filepath(cookies_filepath, name) != 0) { + if(get_cookies_filepath(cookies_filepath, SERVICE_NAME) != 0) { fprintf(stderr, "Failed to get 4chan cookies filepath\n"); return PluginResult::ERR; } @@ -626,7 +490,52 @@ namespace QuickMedia { } } - const std::string& Fourchan::get_pass_id() const { + PostResult FourchanThreadPage::post_comment(const std::string &captcha_id, const std::string &comment) { + std::string url = "https://sys.4chan.org/" + board_id + "/post"; + + std::vector 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", "mode=regist"} + }; + + if(pass_id.empty()) { + additional_args.push_back(CommandArg{"-F", "g-recaptcha-response=" + captcha_id}); + } else { + Path cookies_filepath; + if(get_cookies_filepath(cookies_filepath, SERVICE_NAME) != 0) { + fprintf(stderr, "Failed to get 4chan cookies filepath\n"); + return PostResult::ERR; + } else { + additional_args.push_back(CommandArg{"-c", cookies_filepath.data}); + additional_args.push_back(CommandArg{"-b", cookies_filepath.data}); + } + } + + std::string response; + if(download_to_string(url, response, additional_args, is_tor_enabled(), true) != DownloadResult::OK) + return PostResult::ERR; + + if(response.find("successful") != std::string::npos) + return PostResult::OK; + if(response.find("banned") != std::string::npos) + return PostResult::BANNED; + if(response.find("try again") != std::string::npos || response.find("No valid captcha") != std::string::npos) + return PostResult::TRY_AGAIN; + return PostResult::ERR; + } + + const std::string& FourchanThreadPage::get_pass_id() { + if(pass_id.empty()) { + Path cookies_filepath; + if(get_cookies_filepath(cookies_filepath, SERVICE_NAME) != 0) { + fprintf(stderr, "Failed to get 4chan cookies filepath\n"); + } else { + pass_id = get_pass_id_from_cookies_file(cookies_filepath); + } + } return pass_id; } } \ No newline at end of file diff --git a/src/plugins/ImageBoard.cpp b/src/plugins/ImageBoard.cpp new file mode 100644 index 0000000..ac05f80 --- /dev/null +++ b/src/plugins/ImageBoard.cpp @@ -0,0 +1,30 @@ +#include "../../plugins/ImageBoard.hpp" + +namespace QuickMedia { + BodyItems ImageBoardThreadPage::get_related_media(const std::string &url) { + BodyItems body_items; + auto it = std::find(cached_media_urls.begin(), cached_media_urls.end(), url); + if(it == cached_media_urls.end()) + return body_items; + + ++it; + for(; it != cached_media_urls.end(); ++it) { + auto body_item = BodyItem::create(""); + body_item->url = *it; + body_items.push_back(std::move(body_item)); + } + return body_items; + } + + PluginResult ImageBoardThreadPage::login(const std::string &token, const std::string &pin, std::string &response_msg) { + (void)token; + (void)pin; + response_msg = "Login is not supported on this image board"; + return PluginResult::ERR; + } + + const std::string& ImageBoardThreadPage::get_pass_id() { + static std::string empty_str; + return empty_str; + } +} \ No newline at end of file diff --git a/src/plugins/Manga.cpp b/src/plugins/Manga.cpp index 6ad11ab..70a1664 100644 --- a/src/plugins/Manga.cpp +++ b/src/plugins/Manga.cpp @@ -1,7 +1,12 @@ #include "../../plugins/Manga.hpp" +#include "../../include/Program.h" namespace QuickMedia { - const std::vector& Manga::get_creators() const { - return creators; + TrackResult MangaChaptersPage::track(const std::string &str) { + const char *args[] = { "automedia", "add", "html", content_url.data(), "--start-after", str.data(), "--name", content_title.data(), nullptr }; + if(exec_program(args, nullptr, nullptr) == 0) + return TrackResult::OK; + else + return TrackResult::ERR; } } \ No newline at end of file diff --git a/src/plugins/Mangadex.cpp b/src/plugins/Mangadex.cpp index 9808654..be2d342 100644 --- a/src/plugins/Mangadex.cpp +++ b/src/plugins/Mangadex.cpp @@ -1,6 +1,7 @@ #include "../../plugins/Mangadex.hpp" #include "../../include/Storage.hpp" #include "../../include/Notification.hpp" +#include "../../include/StringUtils.hpp" #include #include @@ -27,30 +28,96 @@ namespace QuickMedia { return url.substr(find_index + 9); } + static bool get_cookie_filepath(std::string &cookie_filepath) { + Path cookie_path = get_storage_dir().join("cookies"); + if(create_directory_recursive(cookie_path) != 0) { + show_notification("Storage", "Failed to create directory: " + cookie_path.data, Urgency::CRITICAL); + return false; + } + cookie_filepath = cookie_path.join("mangadex.txt").data; + return true; + } + struct BodyItemChapterContext { BodyItems *body_items; int prev_chapter_number; bool *is_last_page; }; - SearchResult Mangadex::search(const std::string &url, BodyItems &result_items) { + // TODO: Implement pagination (go to next page and get all results as well) + SearchResult MangadexSearchPage::search(const std::string &str, BodyItems &result_items) { + std::string rememberme_token; + if(!get_rememberme_token(rememberme_token)) + return SearchResult::ERR; + + std::string url = "https://mangadex.org/search?title="; + url += url_param_encode(str); + CommandArg cookie_arg = { "-H", "cookie: mangadex_rememberme_token=" + rememberme_token }; + + std::string website_data; + if(download_to_string(url, website_data, {std::move(cookie_arg)}, is_tor_enabled(), true) != DownloadResult::OK) + return SearchResult::NET_ERR; + + QuickMediaHtmlSearch html_search; + int result = quickmedia_html_search_init(&html_search, website_data.c_str()); + if(result != 0) + goto cleanup; + + result = quickmedia_html_find_nodes_xpath(&html_search, "//a", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *item_data = (BodyItems*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + const char *title = quickmedia_html_node_get_attribute_value(node, "title"); + if(title && href && strncmp(href, "/title/", 7) == 0) { + auto item = BodyItem::create(strip(title)); + item->url = mangadex_url + href; + item_data->push_back(std::move(item)); + } + }, &result_items); + + if(result != 0) + goto cleanup; + + BodyItemImageContext body_item_image_context; + body_item_image_context.body_items = &result_items; + body_item_image_context.index = 0; + + result = quickmedia_html_find_nodes_xpath(&html_search, "//img", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *item_data = (BodyItemImageContext*)userdata; + const char *src = quickmedia_html_node_get_attribute_value(node, "src"); + if(src && strncmp(src, "/images/manga/", 14) == 0 && item_data->index < item_data->body_items->size()) { + (*item_data->body_items)[item_data->index]->thumbnail_url = mangadex_url + src; + item_data->index++; + } + }, &body_item_image_context); + + if(result != 0) + goto cleanup; + + cleanup: + quickmedia_html_search_deinit(&html_search); + return result == 0 ? SearchResult::OK : SearchResult::ERR; + } + + PluginResult MangadexSearchPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { std::string manga_id = title_url_extract_manga_id(url); std::string request_url = "https://mangadex.org/api/?id=" + manga_id + "&type=manga"; Json::Value json_root; DownloadResult result = download_json(json_root, request_url, {}, true); - if(result != DownloadResult::OK) return download_result_to_search_result(result); + if(result != DownloadResult::OK) return download_result_to_plugin_result(result); if(!json_root.isObject()) - return SearchResult::ERR; + return PluginResult::ERR; Json::Value &status_json = json_root["status"]; if(!status_json.isString() || status_json.asString() != "OK") - return SearchResult::ERR; + return PluginResult::ERR; Json::Value &chapter_json = json_root["chapter"]; if(!chapter_json.isObject()) - return SearchResult::ERR; + return PluginResult::ERR; std::vector> chapters(chapter_json.size()); /* TODO: Optimize member access */ @@ -74,6 +141,8 @@ namespace QuickMedia { time_t time_now = time(NULL); + auto body = create_body(); + /* TODO: Pointer */ std::string prev_chapter_number; for(auto it = chapters.begin(); it != chapters.end(); ++it) { @@ -106,22 +175,17 @@ namespace QuickMedia { auto item = BodyItem::create(std::move(chapter_name)); item->url = std::move(chapter_url); - result_items.push_back(std::move(item)); + body->items.push_back(std::move(item)); } - return SearchResult::OK; - } - static bool get_cookie_filepath(std::string &cookie_filepath) { - Path cookie_path = get_storage_dir().join("cookies"); - if(create_directory_recursive(cookie_path) != 0) { - show_notification("Storage", "Failed to create directory: " + cookie_path.data, Urgency::CRITICAL); - return false; - } - cookie_filepath = cookie_path.join("mangadex.txt").data; - return true; + result_tabs.push_back(Tab{std::move(body), std::make_unique(program, title, url), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + if(load_manga_content_storage("mangadex", title, manga_id)) + return PluginResult::OK; + return PluginResult::ERR; } - bool Mangadex::get_rememberme_token(std::string &rememberme_token_output) { + bool MangadexSearchPage::get_rememberme_token(std::string &rememberme_token_output) { if(rememberme_token) { rememberme_token_output = rememberme_token.value(); return true; @@ -153,93 +217,34 @@ namespace QuickMedia { return true; } - struct BodyItemImageContext { - BodyItems *body_items; - size_t index; - }; - - // TODO: Implement pagination (go to next page and get all results as well) - SuggestionResult Mangadex::update_search_suggestions(const std::string &text, BodyItems &result_items) { - std::string rememberme_token; - if(!get_rememberme_token(rememberme_token)) - return SuggestionResult::ERR; - - std::string url = "https://mangadex.org/search?title="; - url += url_param_encode(text); - CommandArg cookie_arg = { "-H", "cookie: mangadex_rememberme_token=" + rememberme_token }; - - std::string website_data; - if(download_to_string(url, website_data, {std::move(cookie_arg)}, use_tor, true) != DownloadResult::OK) - return SuggestionResult::NET_ERR; - - QuickMediaHtmlSearch html_search; - int result = quickmedia_html_search_init(&html_search, website_data.c_str()); - if(result != 0) - goto cleanup; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//a", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItems*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *title = quickmedia_html_node_get_attribute_value(node, "title"); - if(title && href && strncmp(href, "/title/", 7) == 0) { - auto item = BodyItem::create(strip(title)); - item->url = mangadex_url + href; - item_data->push_back(std::move(item)); - } - }, &result_items); - - if(result != 0) - goto cleanup; - - BodyItemImageContext body_item_image_context; - body_item_image_context.body_items = &result_items; - body_item_image_context.index = 0; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//img", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItemImageContext*)userdata; - const char *src = quickmedia_html_node_get_attribute_value(node, "src"); - if(src && strncmp(src, "/images/manga/", 14) == 0 && item_data->index < item_data->body_items->size()) { - (*item_data->body_items)[item_data->index]->thumbnail_url = mangadex_url + src; - item_data->index++; - } - }, &body_item_image_context); - - if(result != 0) - goto cleanup; - - cleanup: - quickmedia_html_search_deinit(&html_search); - return result == 0 ? SuggestionResult::OK : SuggestionResult::ERR; + PluginResult MangadexChaptersPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back(Tab{create_body(), std::make_unique(program, content_title, title, url), nullptr}); + return PluginResult::OK; } - ImageResult Mangadex::get_number_of_images(const std::string &url, int &num_images) { - std::lock_guard lock(image_urls_mutex); + ImageResult MangadexImagesPage::get_number_of_images(int &num_images) { num_images = 0; ImageResult image_result = get_image_urls_for_chapter(url); if(image_result != ImageResult::OK) return image_result; - num_images = last_chapter_image_urls.size(); + num_images = chapter_image_urls.size(); return ImageResult::OK; } - bool Mangadex::save_mangadex_cookies(const std::string &url, const std::string &cookie_filepath) { + bool MangadexImagesPage::save_mangadex_cookies(const std::string &url, const std::string &cookie_filepath) { CommandArg cookie_arg = { "-c", cookie_filepath }; std::string server_response; - if(download_to_string(url, server_response, {std::move(cookie_arg)}, use_tor, true) != DownloadResult::OK) + if(download_to_string(url, server_response, {std::move(cookie_arg)}, is_tor_enabled(), true) != DownloadResult::OK) return false; return true; } - ImageResult Mangadex::get_image_urls_for_chapter(const std::string &url) { - if(url == last_chapter_url) + ImageResult MangadexImagesPage::get_image_urls_for_chapter(const std::string &url) { + if(!chapter_image_urls.empty()) return ImageResult::OK; - last_chapter_image_urls.clear(); - std::string cookie_filepath; if(!get_cookie_filepath(cookie_filepath)) return ImageResult::ERR; @@ -281,38 +286,28 @@ namespace QuickMedia { continue; std::string image_url = server + chapter_hash_str + "/" + image_name.asCString(); - last_chapter_image_urls.push_back(std::move(image_url)); + chapter_image_urls.push_back(std::move(image_url)); } } - last_chapter_url = url; - if(last_chapter_image_urls.empty()) { - last_chapter_url.clear(); + if(chapter_image_urls.empty()) return ImageResult::ERR; - } return ImageResult::OK; } - ImageResult Mangadex::for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) { + ImageResult MangadexImagesPage::for_each_page_in_chapter(PageCallback callback) { std::vector image_urls; - { - std::lock_guard lock(image_urls_mutex); - ImageResult image_result = get_image_urls_for_chapter(chapter_url); - if(image_result != ImageResult::OK) - return image_result; + ImageResult image_result = get_image_urls_for_chapter(url); + if(image_result != ImageResult::OK) + return image_result; - image_urls = last_chapter_image_urls; - } + image_urls = chapter_image_urls; for(const std::string &url : image_urls) { if(!callback(url)) break; } - return ImageResult::OK; - } - bool Mangadex::extract_id_from_url(const std::string &url, std::string &manga_id) { - manga_id = title_url_extract_manga_id(url); - return true; + return ImageResult::OK; } } diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp index 52b9ebd..e96bc65 100644 --- a/src/plugins/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -1,56 +1,10 @@ #include "../../plugins/Manganelo.hpp" #include "../../include/Notification.hpp" +#include "../../include/StringUtils.hpp" #include namespace QuickMedia { - struct BodyItemImageContext { - BodyItems *body_items; - size_t index; - }; - - SearchResult Manganelo::search(const std::string &url, BodyItems &result_items) { - creators.clear(); - - std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return SearchResult::NET_ERR; - - QuickMediaHtmlSearch html_search; - int result = quickmedia_html_search_init(&html_search, website_data.c_str()); - if(result != 0) - goto cleanup; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//ul[class='row-content-chapter']//a", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItems*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *text = quickmedia_html_node_get_text(node); - if(href && text) { - auto item = BodyItem::create(strip(text)); - item->url = href; - item_data->push_back(std::move(item)); - } - }, &result_items); - - quickmedia_html_find_nodes_xpath(&html_search, "//a[class='a-h']", - [](QuickMediaHtmlNode *node, void *userdata) { - std::vector *creators = (std::vector*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *text = quickmedia_html_node_get_text(node); - if(href && text && strstr(href, "/author/story/")) { - Creator creator; - creator.name = strip(text); - creator.url = href; - creators->push_back(std::move(creator)); - } - }, &creators); - - cleanup: - quickmedia_html_search_deinit(&html_search); - return result == 0 ? SearchResult::OK : SearchResult::ERR; - } - - // Returns true if changed + // Returns true if modified static bool remove_html_span(std::string &str) { size_t open_tag_start = str.find("url = "https://manganelo.com/manga/" + url_param_encode(nameunsigned.asString()); - Json::Value image = child.get("image", ""); - if(image.isString() && image.asCString()[0] != '\0') - item->thumbnail_url = image.asString(); - result_items.push_back(std::move(item)); - } - } + if(result != DownloadResult::OK) return download_result_to_search_result(result); + + if(json_root.isNull()) + return SearchResult::OK; + + if(!json_root.isArray()) + return SearchResult::ERR; + + for(const Json::Value &child : json_root) { + if(!child.isObject()) + continue; + + Json::Value name = child.get("name", ""); + Json::Value nameunsigned = child.get("nameunsigned", ""); + if(name.isString() && name.asCString()[0] != '\0' && nameunsigned.isString() && nameunsigned.asCString()[0] != '\0') { + std::string name_str = name.asString(); + while(remove_html_span(name_str)) {} + auto item = BodyItem::create(strip(name_str)); + item->url = "https://manganelo.com/manga/" + url_param_encode(nameunsigned.asString()); + Json::Value image = child.get("image", ""); + if(image.isString() && image.asCString()[0] != '\0') + item->thumbnail_url = image.asString(); + result_items.push_back(std::move(item)); } } - return SuggestionResult::OK; - } - ImageResult Manganelo::get_number_of_images(const std::string &url, int &num_images) { - std::lock_guard lock(image_urls_mutex); - num_images = 0; - ImageResult image_result = get_image_urls_for_chapter(url); - if(image_result != ImageResult::OK) - return image_result; - - num_images = last_chapter_image_urls.size(); - return ImageResult::OK; + return SearchResult::OK; } - ImageResult Manganelo::get_image_urls_for_chapter(const std::string &url) { - if(url == last_chapter_url) - return ImageResult::OK; - - last_chapter_image_urls.clear(); + PluginResult ManganeloSearchPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + BodyItems chapters_items; + std::vector creators; std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return ImageResult::NET_ERR; + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) + return PluginResult::NET_ERR; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str()); if(result != 0) goto cleanup; - result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='container-chapter-reader']/img", + result = quickmedia_html_find_nodes_xpath(&html_search, "//ul[class='row-content-chapter']//a", [](QuickMediaHtmlNode *node, void *userdata) { - auto *urls = (std::vector*)userdata; - const char *src = quickmedia_html_node_get_attribute_value(node, "src"); - if(src) { - std::string image_url = src; - urls->emplace_back(std::move(image_url)); + auto *item_data = (BodyItems*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + const char *text = quickmedia_html_node_get_text(node); + if(href && text) { + auto item = BodyItem::create(strip(text)); + item->url = href; + item_data->push_back(std::move(item)); + } + }, &chapters_items); + + quickmedia_html_find_nodes_xpath(&html_search, "//a[class='a-h']", + [](QuickMediaHtmlNode *node, void *userdata) { + std::vector *creators = (std::vector*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + const char *text = quickmedia_html_node_get_text(node); + if(href && text && strstr(href, "/author/story/")) { + Creator creator; + creator.name = strip(text); + creator.url = href; + creators->push_back(std::move(creator)); } - }, &last_chapter_image_urls); + }, &creators); cleanup: quickmedia_html_search_deinit(&html_search); - if(result == 0) - last_chapter_url = url; - if(last_chapter_image_urls.empty()) { - last_chapter_url.clear(); - return ImageResult::ERR; - } - return result == 0 ? ImageResult::OK : ImageResult::ERR; - } + if(result != 0) + return PluginResult::ERR; - ImageResult Manganelo::for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) { - std::vector image_urls; - { - std::lock_guard lock(image_urls_mutex); - ImageResult image_result = get_image_urls_for_chapter(chapter_url); - if(image_result != ImageResult::OK) - return image_result; + auto chapters_body = create_body(); + chapters_body->items = std::move(chapters_items); + result_tabs.push_back(Tab{std::move(chapters_body), std::make_unique(program, title, url), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); - image_urls = last_chapter_image_urls; + for(Creator &creator : creators) { + result_tabs.push_back(Tab{create_body(), std::make_unique(program, std::move(creator)), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } - for(const std::string &url : image_urls) { - if(!callback(url)) - break; + std::string manga_id; + if(extract_id_from_url(url, manga_id)) { + if(load_manga_content_storage("manganelo", title, manga_id)) + return PluginResult::OK; } - return ImageResult::OK; + return PluginResult::ERR; } - - bool Manganelo::extract_id_from_url(const std::string &url, std::string &manga_id) { + + bool ManganeloSearchPage::extract_id_from_url(const std::string &url, std::string &manga_id) const { bool manganelo_website = false; if(url.find("mangakakalot") != std::string::npos || url.find("manganelo") != std::string::npos) manganelo_website = true; @@ -177,7 +131,7 @@ namespace QuickMedia { std::string err_msg = "Url "; err_msg += url; err_msg += " doesn't contain manga id"; - show_notification("Manga", err_msg, Urgency::CRITICAL); + show_notification("QuickMedia", err_msg, Urgency::CRITICAL); return false; } @@ -186,7 +140,7 @@ namespace QuickMedia { std::string err_msg = "Url "; err_msg += url; err_msg += " doesn't contain manga id"; - show_notification("Manga", err_msg, Urgency::CRITICAL); + show_notification("QuickMedia", err_msg, Urgency::CRITICAL); return false; } return true; @@ -194,56 +148,81 @@ namespace QuickMedia { std::string err_msg = "Unexpected url "; err_msg += url; err_msg += " is not manganelo or mangakakalot"; - show_notification("Manga", err_msg, Urgency::CRITICAL); + show_notification("QuickMedia", err_msg, Urgency::CRITICAL); return false; } } - PluginResult Manganelo::get_creators_manga_list(const std::string &url, BodyItems &result_items) { + PluginResult ManganeloChaptersPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back(Tab{create_body(), std::make_unique(program, content_title, title, url), nullptr}); + return PluginResult::OK; + } + + PluginResult ManganeloCreatorPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + (void)title; + (void)url; + (void)result_tabs; + // TODO: Implement + return PluginResult::ERR; + } + + ImageResult ManganeloImagesPage::get_number_of_images(int &num_images) { + num_images = 0; + ImageResult image_result = get_image_urls_for_chapter(url); + if(image_result != ImageResult::OK) + return image_result; + + num_images = chapter_image_urls.size(); + return ImageResult::OK; + } + + ImageResult ManganeloImagesPage::for_each_page_in_chapter(PageCallback callback) { + std::vector image_urls; + ImageResult image_result = get_image_urls_for_chapter(url); + if(image_result != ImageResult::OK) + return image_result; + + image_urls = chapter_image_urls; + + for(const std::string &url : image_urls) { + if(!callback(url)) + break; + } + + return ImageResult::OK; + } + + ImageResult ManganeloImagesPage::get_image_urls_for_chapter(const std::string &url) { + if(!chapter_image_urls.empty()) + return ImageResult::OK; + std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return PluginResult::NET_ERR; + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) + return ImageResult::NET_ERR; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str()); if(result != 0) goto cleanup; - result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='search-story-item']//a[class='item-img']", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItems*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *title = quickmedia_html_node_get_attribute_value(node, "title"); - if(href && title && strstr(href, "/manga/")) { - auto body_item = BodyItem::create(title); - body_item->url = href; - item_data->push_back(std::move(body_item)); - } - }, &result_items); - - if(result != 0) - goto cleanup; - - BodyItemImageContext body_item_image_context; - body_item_image_context.body_items = &result_items; - body_item_image_context.index = 0; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='search-story-item']//a[class='item-img']//img", + result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='container-chapter-reader']/img", [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItemImageContext*)userdata; + auto *urls = (std::vector*)userdata; const char *src = quickmedia_html_node_get_attribute_value(node, "src"); - if(src && item_data->index < item_data->body_items->size()) { - (*item_data->body_items)[item_data->index]->thumbnail_url = src; - item_data->index++; + if(src) { + std::string image_url = src; + urls->push_back(std::move(image_url)); } - }, &body_item_image_context); + }, &chapter_image_urls); cleanup: quickmedia_html_search_deinit(&html_search); if(result != 0) { - result_items.clear(); - return PluginResult::ERR; + chapter_image_urls.clear(); + return ImageResult::ERR; } - return PluginResult::OK; + if(chapter_image_urls.empty()) + return ImageResult::ERR; + return ImageResult::OK; } } \ No newline at end of file diff --git a/src/plugins/Mangatown.cpp b/src/plugins/Mangatown.cpp index 400d1ef..5d6f97f 100644 --- a/src/plugins/Mangatown.cpp +++ b/src/plugins/Mangatown.cpp @@ -1,59 +1,26 @@ #include "../../plugins/Mangatown.hpp" #include "../../include/Notification.hpp" +#include "../../include/StringUtils.hpp" #include static const std::string mangatown_url = "https://www.mangatown.com"; namespace QuickMedia { - SearchResult Mangatown::search(const std::string &url, BodyItems &result_items) { - std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return SearchResult::NET_ERR; - - QuickMediaHtmlSearch html_search; - int result = quickmedia_html_search_init(&html_search, website_data.c_str()); - if(result != 0) - goto cleanup; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//ul[class='chapter_list']//a", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *item_data = (BodyItems*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *text = quickmedia_html_node_get_text(node); - if(href && text && strncmp(href, "/manga/", 7) == 0) { - auto item = BodyItem::create(strip(text)); - item->url = mangatown_url + href; - item_data->push_back(std::move(item)); - } - }, &result_items); - - cleanup: - quickmedia_html_search_deinit(&html_search); - - int chapter_num = result_items.size(); - for(auto &body_item : result_items) { - body_item->set_title("Ch. " + std::to_string(chapter_num)); - chapter_num--; - } - - return result == 0 ? SearchResult::OK : SearchResult::ERR; + static bool is_number_with_zero_fill(const char *str) { + while(*str == '0') { ++str; } + return atoi(str) != 0; } - struct BodyItemImageContext { - BodyItems *body_items; - size_t index; - }; - - SuggestionResult Mangatown::update_search_suggestions(const std::string &text, BodyItems &result_items) { + SearchResult MangatownSearchPage::search(const std::string &str, BodyItems &result_items) { std::string url = "https://www.mangatown.com/search?name="; - url += url_param_encode(text); + url += url_param_encode(str); std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return SuggestionResult::NET_ERR; + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) + return SearchResult::NET_ERR; if(website_data.empty()) - return SuggestionResult::OK; + return SearchResult::OK; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str()); @@ -88,77 +55,101 @@ namespace QuickMedia { cleanup: quickmedia_html_search_deinit(&html_search); - return SuggestionResult::OK; + return SearchResult::OK; } - static bool is_number_with_zero_fill(const char *str) { - while(*str == '0') { ++str; } - return atoi(str) != 0; - } - - ImageResult Mangatown::get_number_of_images(const std::string &url, int &num_images) { - std::lock_guard lock(image_urls_mutex); - - num_images = last_num_pages; - if(url == last_chapter_url_num_images) - return ImageResult::OK; - - last_num_pages = 0; + PluginResult MangatownSearchPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + BodyItems chapters_items; std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) - return ImageResult::NET_ERR; + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) + return PluginResult::NET_ERR; QuickMediaHtmlSearch html_search; int result = quickmedia_html_search_init(&html_search, website_data.c_str()); if(result != 0) goto cleanup; - result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='page_select']//option", + result = quickmedia_html_find_nodes_xpath(&html_search, "//ul[class='chapter_list']//a", [](QuickMediaHtmlNode *node, void *userdata) { - int *last_num_pages = (int*)userdata; - const char *value = quickmedia_html_node_get_attribute_value(node, "value"); + auto *item_data = (BodyItems*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); const char *text = quickmedia_html_node_get_text(node); - if(value && strncmp(value, "/manga/", 7) == 0) { - if(is_number_with_zero_fill(text)) { - (*last_num_pages)++; - } + if(href && text && strncmp(href, "/manga/", 7) == 0) { + auto item = BodyItem::create(strip(text)); + item->url = mangatown_url + href; + item_data->push_back(std::move(item)); } - }, &last_num_pages); - - last_num_pages /= 2; - num_images = last_num_pages; + }, &chapters_items); cleanup: quickmedia_html_search_deinit(&html_search); + if(result != 0) + return PluginResult::ERR; - if(result == 0) - last_chapter_url_num_images = url; - if(last_num_pages == 0) { - last_chapter_url_num_images.clear(); - return ImageResult::ERR; + int chapter_num = chapters_items.size(); + for(auto &body_item : chapters_items) { + body_item->set_title("Ch. " + std::to_string(chapter_num)); + chapter_num--; } - return result == 0 ? ImageResult::OK : ImageResult::ERR; + + auto body = create_body(); + body->items = std::move(chapters_items); + result_tabs.push_back(Tab{std::move(body), std::make_unique(program, title, url), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + + std::string manga_id; + if(extract_id_from_url(url, manga_id)) { + if(load_manga_content_storage("mangatown", title, manga_id)) + return PluginResult::OK; + } + return PluginResult::ERR; + } + + bool MangatownSearchPage::extract_id_from_url(const std::string &url, std::string &manga_id) const { + size_t start_index = url.find("/manga/"); + if(start_index == std::string::npos) + return false; + + start_index += 7; + size_t end_index = url.find("/", start_index); + if(end_index == std::string::npos) { + manga_id = url.substr(start_index); + return true; + } + + manga_id = url.substr(start_index, end_index - start_index); + return true; } - ImageResult Mangatown::for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) { - int num_pages; - ImageResult image_result = get_number_of_images(chapter_url, num_pages); + PluginResult MangatownChaptersPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back(Tab{create_body(), std::make_unique(program, content_title, title, url), nullptr}); + return PluginResult::OK; + } + + ImageResult MangatownImagesPage::get_number_of_images(int &num_images) { + num_images = 0; + ImageResult image_result = get_image_urls_for_chapter(url); if(image_result != ImageResult::OK) return image_result; - int result = 0; - int page_index = 1; + num_images = chapter_image_urls.size(); + return ImageResult::OK; + } + + ImageResult MangatownImagesPage::for_each_page_in_chapter(PageCallback callback) { + int num_pages; + ImageResult image_result = get_number_of_images(num_pages); + if(image_result != ImageResult::OK) + return image_result; - while(true) { + for(const std::string &full_url : chapter_image_urls) { std::string image_src; std::string website_data; - std::string full_url = chapter_url + std::to_string(page_index++) + ".html"; - if(download_to_string_cache(full_url, website_data, {}, use_tor, true) != DownloadResult::OK) + if(download_to_string_cache(full_url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) break; QuickMediaHtmlSearch html_search; - result = quickmedia_html_search_init(&html_search, website_data.c_str()); + int result = quickmedia_html_search_init(&html_search, website_data.c_str()); if(result != 0) goto cleanup; @@ -190,19 +181,46 @@ namespace QuickMedia { return ImageResult::OK; } - bool Mangatown::extract_id_from_url(const std::string &url, std::string &manga_id) { - size_t start_index = url.find("/manga/"); - if(start_index == std::string::npos) - return false; - - start_index += 7; - size_t end_index = url.find("/", start_index); - if(end_index == std::string::npos) { - manga_id = url.substr(start_index); - return true; + ImageResult MangatownImagesPage::get_image_urls_for_chapter(const std::string &url) { + if(!chapter_image_urls.empty()) + return ImageResult::OK; + + int num_pages = 0; + + std::string website_data; + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) + return ImageResult::NET_ERR; + + QuickMediaHtmlSearch html_search; + int result = quickmedia_html_search_init(&html_search, website_data.c_str()); + if(result != 0) + goto cleanup; + + result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class='page_select']//option", + [](QuickMediaHtmlNode *node, void *userdata) { + int *last_num_pages = (int*)userdata; + const char *value = quickmedia_html_node_get_attribute_value(node, "value"); + const char *text = quickmedia_html_node_get_text(node); + if(value && strncmp(value, "/manga/", 7) == 0) { + if(is_number_with_zero_fill(text)) { + (*last_num_pages)++; + } + } + }, &num_pages); + + num_pages /= 2; + + cleanup: + quickmedia_html_search_deinit(&html_search); + if(result != 0) { + chapter_image_urls.clear(); + return ImageResult::ERR; } - - manga_id = url.substr(start_index, end_index - start_index); - return true; + if(num_pages == 0) + return ImageResult::ERR; + for(int i = 0; i < num_pages; ++i) { + chapter_image_urls.push_back(url + std::to_string(1 + i) + ".html"); + } + return ImageResult::OK; } } \ No newline at end of file diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index 2107812..dec4a68 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -1,5 +1,6 @@ #include "../../plugins/Matrix.hpp" #include "../../include/Storage.hpp" +#include "../../include/StringUtils.hpp" #include #include #include @@ -18,6 +19,8 @@ // TODO: Verify if this class really is thread-safe (for example room data fields, user fields, message fields; etc that are updated in /sync) +static const char* SERVICE_NAME = "matrix"; + namespace QuickMedia { std::shared_ptr RoomData::get_user_by_id(const std::string &user_id) { std::lock_guard lock(room_mutex); @@ -99,10 +102,6 @@ namespace QuickMedia { return messages; } - Matrix::Matrix() : Plugin("matrix") { - - } - PluginResult Matrix::sync(RoomSyncMessages &room_messages) { std::vector additional_args = { { "-H", "Authorization: Bearer " + access_token }, @@ -819,10 +818,6 @@ namespace QuickMedia { return PluginResult::OK; } - SearchResult Matrix::search(const std::string&, BodyItems&) { - return SearchResult::OK; - } - static bool generate_random_characters(char *buffer, int buffer_size) { int fd = open("/dev/urandom", O_RDONLY); if(fd == -1) { @@ -1443,7 +1438,7 @@ namespace QuickMedia { // TODO: Handle well_known field. The spec says clients SHOULD handle it if its provided - Path session_path = get_storage_dir().join(name); + Path session_path = get_storage_dir().join(SERVICE_NAME); if(create_directory_recursive(session_path) == 0) { session_path.join("session.json"); if(!save_json_to_file_atomic(session_path, json_root)) { @@ -1457,7 +1452,7 @@ namespace QuickMedia { } PluginResult Matrix::logout() { - Path session_path = get_storage_dir().join(name).join("session.json"); + Path session_path = get_storage_dir().join(SERVICE_NAME).join("session.json"); remove(session_path.data.c_str()); std::vector additional_args = { @@ -1530,7 +1525,7 @@ namespace QuickMedia { } PluginResult Matrix::load_and_verify_cached_session() { - Path session_path = get_storage_dir().join(name).join("session.json"); + Path session_path = get_storage_dir().join(SERVICE_NAME).join("session.json"); std::string session_json_content; if(file_get_content(session_path, session_json_content) != 0) { fprintf(stderr, "Info: failed to read matrix session from %s. Either its missing or we failed to read the file\n", session_path.data.c_str()); @@ -1721,4 +1716,28 @@ namespace QuickMedia { std::lock_guard lock(room_data_mutex); room_data_by_id.insert(std::make_pair(room->id, room)); } + + DownloadResult Matrix::download_json(Json::Value &result, const std::string &url, std::vector additional_args, bool use_browser_useragent, std::string *err_msg) const { + std::string server_response; + if(download_to_string(url, server_response, std::move(additional_args), use_tor, use_browser_useragent, err_msg == nullptr) != DownloadResult::OK) { + if(err_msg) + *err_msg = server_response; + return DownloadResult::NET_ERR; + } + + if(server_response.empty()) + return DownloadResult::OK; + + Json::CharReaderBuilder json_builder; + std::unique_ptr json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(&server_response[0], &server_response[server_response.size()], &result, &json_errors)) { + fprintf(stderr, "download_json error: %s\n", json_errors.c_str()); + if(err_msg) + *err_msg = std::move(json_errors); + return DownloadResult::ERR; + } + + return DownloadResult::OK; + } } \ No newline at end of file diff --git a/src/plugins/NyaaSi.cpp b/src/plugins/NyaaSi.cpp index e29b2b0..8d0679e 100644 --- a/src/plugins/NyaaSi.cpp +++ b/src/plugins/NyaaSi.cpp @@ -1,5 +1,8 @@ #include "../../plugins/NyaaSi.hpp" #include "../../include/Program.h" +#include "../../include/Storage.hpp" +#include "../../include/Notification.hpp" +#include "../../include/StringUtils.hpp" #include namespace QuickMedia { @@ -29,60 +32,19 @@ namespace QuickMedia { return true; } - NyaaSi::NyaaSi() : Plugin("nyaa.si") { - - } - - NyaaSi::~NyaaSi() { - - } - static std::shared_ptr create_front_page_item(const std::string &title, const std::string &category) { auto body_item = BodyItem::create(title); body_item->url = category; return body_item; } - PluginResult NyaaSi::get_front_page(BodyItems &result_items) { - result_items.push_back(create_front_page_item("All categories", "0_0")); - result_items.push_back(create_front_page_item("Anime", "1_0")); - result_items.push_back(create_front_page_item(" Anime - Music video", "1_1")); - result_items.push_back(create_front_page_item(" Anime - English translated", "1_2")); - result_items.push_back(create_front_page_item(" Anime - Non-english translated", "1_3")); - result_items.push_back(create_front_page_item(" Anime - Raw", "1_4")); - result_items.push_back(create_front_page_item("Audio", "2_0")); - result_items.push_back(create_front_page_item(" Audio - Lossless", "2_1")); - result_items.push_back(create_front_page_item(" Anime - Lossy", "2_2")); - result_items.push_back(create_front_page_item("Literature", "3_0")); - result_items.push_back(create_front_page_item(" Literature - English translated", "3_1")); - result_items.push_back(create_front_page_item(" Literature - Non-english translated", "3_1")); - result_items.push_back(create_front_page_item(" Literature - Raw", "3_3")); - result_items.push_back(create_front_page_item("Live Action", "4_0")); - result_items.push_back(create_front_page_item(" Live Action - English translated", "4_1")); - result_items.push_back(create_front_page_item(" Live Action - Non-english translated", "4_3")); - result_items.push_back(create_front_page_item(" Live Action - Idol/Promotional video", "4_2")); - result_items.push_back(create_front_page_item(" Live Action - Raw", "4_4")); - result_items.push_back(create_front_page_item("Pictures", "5_0")); - result_items.push_back(create_front_page_item(" Pictures - Graphics", "5_1")); - result_items.push_back(create_front_page_item(" Pictures - Photos", "5_2")); - result_items.push_back(create_front_page_item("Software", "6_0")); - result_items.push_back(create_front_page_item(" Software - Applications", "6_1")); - result_items.push_back(create_front_page_item(" Software - Games", "6_2")); - return PluginResult::OK; - } - - - SearchResult NyaaSi::content_list_search(const std::string &list_url, const std::string &text, BodyItems &result_items) { - return search_page(list_url, text, 1, result_items); - } - - SearchResult NyaaSi::content_list_search_page(const std::string &list_url, const std::string &text, int page, BodyItems &result_items) { - return search_page(list_url, text, 1 + page, result_items); + static PluginResult search_result_to_plugin_result(SearchResult search_result) { + return (PluginResult)search_result; } // TODO: Also show the number of comments for each torrent. TODO: Optimize? // TODO: Show each field as seperate columns instead of seperating by | - SearchResult NyaaSi::search_page(const std::string &list_url, const std::string &text, int page, BodyItems &result_items) { + static SearchResult search_page(const std::string &list_url, const std::string &text, int page, bool use_tor, BodyItems &result_items) { std::string full_url = "https://nyaa.si/?c=" + list_url + "&f=0&p=" + std::to_string(page) + "&q="; full_url += url_param_encode(text); @@ -217,34 +179,64 @@ namespace QuickMedia { return SearchResult::OK; } - static PluginResult search_result_to_plugin_result(SearchResult search_result) { - switch(search_result) { - case SearchResult::OK: return PluginResult::OK; - case SearchResult::ERR: return PluginResult::ERR; - case SearchResult::NET_ERR: return PluginResult::NET_ERR; - } - return PluginResult::ERR; + PluginResult NyaaSiCategoryPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + BodyItems result_items; + SearchResult search_result = search_page(url, "", 1, is_tor_enabled(), result_items); + if(search_result != SearchResult::OK) return search_result_to_plugin_result(search_result); + + auto body = create_body(); + body->items = std::move(result_items); + body->draw_thumbnails = true; + result_tabs.push_back(Tab{std::move(body), std::make_unique(program, strip(title), url), create_search_bar("Search...", 200)}); + return PluginResult::OK; } - PluginResult NyaaSi::get_content_list(const std::string &url, BodyItems &result_items) { - return search_result_to_plugin_result(search_page(url, "", 1, result_items)); + void NyaaSiCategoryPage::get_categories(BodyItems &result_items) { + result_items.push_back(create_front_page_item("All categories", "0_0")); + result_items.push_back(create_front_page_item("Anime", "1_0")); + result_items.push_back(create_front_page_item(" Anime - Music video", "1_1")); + result_items.push_back(create_front_page_item(" Anime - English translated", "1_2")); + result_items.push_back(create_front_page_item(" Anime - Non-english translated", "1_3")); + result_items.push_back(create_front_page_item(" Anime - Raw", "1_4")); + result_items.push_back(create_front_page_item("Audio", "2_0")); + result_items.push_back(create_front_page_item(" Audio - Lossless", "2_1")); + result_items.push_back(create_front_page_item(" Anime - Lossy", "2_2")); + result_items.push_back(create_front_page_item("Literature", "3_0")); + result_items.push_back(create_front_page_item(" Literature - English translated", "3_1")); + result_items.push_back(create_front_page_item(" Literature - Non-english translated", "3_1")); + result_items.push_back(create_front_page_item(" Literature - Raw", "3_3")); + result_items.push_back(create_front_page_item("Live Action", "4_0")); + result_items.push_back(create_front_page_item(" Live Action - English translated", "4_1")); + result_items.push_back(create_front_page_item(" Live Action - Non-english translated", "4_3")); + result_items.push_back(create_front_page_item(" Live Action - Idol/Promotional video", "4_2")); + result_items.push_back(create_front_page_item(" Live Action - Raw", "4_4")); + result_items.push_back(create_front_page_item("Pictures", "5_0")); + result_items.push_back(create_front_page_item(" Pictures - Graphics", "5_1")); + result_items.push_back(create_front_page_item(" Pictures - Photos", "5_2")); + result_items.push_back(create_front_page_item("Software", "6_0")); + result_items.push_back(create_front_page_item(" Software - Applications", "6_1")); + result_items.push_back(create_front_page_item(" Software - Games", "6_2")); } - struct BodyItemImageContext { - BodyItems *body_items; - size_t index; - }; + SearchResult NyaaSiSearchPage::search(const std::string &str, BodyItems &result_items) { + return search_page(category_id, str, 1, is_tor_enabled(), result_items); + } - PluginResult NyaaSi::get_content_details(const std::string&, const std::string &url, BodyItems &result_items) { + PluginResult NyaaSiSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { + return search_result_to_plugin_result(search_page(category_id, str, 1 + page, is_tor_enabled(), result_items)); + } + + PluginResult NyaaSiSearchPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { size_t comments_start_index; std::string title; + BodyItems result_items; auto torrent_item = BodyItem::create("Download magnet"); std::string magnet_url; std::string description; std::string website_data; - if(download_to_string(url, website_data, {}, use_tor, true) != DownloadResult::OK) + if(download_to_string(url, website_data, {}, is_tor_enabled(), true) != DownloadResult::OK) return PluginResult::NET_ERR; QuickMediaHtmlSearch html_search; @@ -377,9 +369,26 @@ namespace QuickMedia { cleanup: quickmedia_html_search_deinit(&html_search); - if(result != 0) { - result_items.clear(); + if(result != 0) return PluginResult::ERR; + + auto body = create_body(); + body->items = std::move(result_items); + body->draw_thumbnails = true; + result_tabs.push_back(Tab{std::move(body), std::make_unique(program), nullptr}); + return PluginResult::OK; + } + + PluginResult NyaaSiTorrentPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + (void)title; + (void)result_tabs; + if(strncmp(url.c_str(), "magnet:?", 8) == 0) { + if(!is_program_executable_by_name("xdg-open")) { + show_notification("Nyaa.si", "xdg-utils which provides xdg-open needs to be installed to download torrents", Urgency::CRITICAL); + return PluginResult::ERR; + } + const char *args[] = { "xdg-open", url.c_str(), nullptr }; + exec_program_async(args, nullptr); } return PluginResult::OK; } diff --git a/src/plugins/Page.cpp b/src/plugins/Page.cpp new file mode 100644 index 0000000..48efeff --- /dev/null +++ b/src/plugins/Page.cpp @@ -0,0 +1,50 @@ +#include "../../plugins/Page.hpp" +#include "../../include/QuickMedia.hpp" +#include + +namespace QuickMedia { + BodyItems Page::get_related_media(const std::string &url) { + (void)url; + return {}; + } + + DownloadResult Page::download_json(Json::Value &result, const std::string &url, std::vector additional_args, bool use_browser_useragent, std::string *err_msg) { + std::string server_response; + if(download_to_string(url, server_response, std::move(additional_args), is_tor_enabled(), use_browser_useragent, err_msg == nullptr) != DownloadResult::OK) { + if(err_msg) + *err_msg = server_response; + return DownloadResult::NET_ERR; + } + + if(server_response.empty()) + return DownloadResult::OK; + + Json::CharReaderBuilder json_builder; + std::unique_ptr json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(&server_response[0], &server_response[server_response.size()], &result, &json_errors)) { + fprintf(stderr, "download_json error: %s\n", json_errors.c_str()); + if(err_msg) + *err_msg = std::move(json_errors); + return DownloadResult::ERR; + } + + return DownloadResult::OK; + } + + bool Page::is_tor_enabled() { + return program->is_tor_enabled(); + } + + std::unique_ptr Page::create_body() { + return program->create_body(); + } + + std::unique_ptr Page::create_search_bar(const std::string &placeholder_text, int search_delay) { + return program->create_search_bar(placeholder_text, search_delay); + } + + bool Page::load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id) { + return program->load_manga_content_storage(service_name, manga_title, manga_id); + } +} \ No newline at end of file diff --git a/src/plugins/Plugin.cpp b/src/plugins/Plugin.cpp index ac60187..3f76b4c 100644 --- a/src/plugins/Plugin.cpp +++ b/src/plugins/Plugin.cpp @@ -1,34 +1,10 @@ #include "../../plugins/Plugin.hpp" +#include "../../include/StringUtils.hpp" #include #include #include -#include namespace QuickMedia { - SearchResult Plugin::search(const std::string &text, BodyItems &result_items) { - (void)text; - (void)result_items; - return SearchResult::OK; - } - - SuggestionResult Plugin::update_search_suggestions(const std::string &text, BodyItems &result_items) { - (void)text; - (void)result_items; - return SuggestionResult::OK; - } - - SearchResult Plugin::content_list_search(const std::string &list_url, const std::string &text, BodyItems &result_items) { - (void)list_url; - (void)text; - (void)result_items; - return SearchResult::OK; - } - - BodyItems Plugin::get_related_media(const std::string &url) { - (void)url; - return {}; - } - struct HtmlEscapeSequence { char unescape_char; std::string escape_sequence; @@ -69,7 +45,7 @@ namespace QuickMedia { } } - std::string Plugin::url_param_encode(const std::string ¶m) const { + std::string url_param_encode(const std::string ¶m) { std::ostringstream result; result.fill('0'); result << std::hex; @@ -86,24 +62,8 @@ namespace QuickMedia { return result.str(); } - DownloadResult Plugin::download_json(Json::Value &result, const std::string &url, std::vector additional_args, bool use_browser_useragent, std::string *err_msg) const { - std::string server_response; - if(download_to_string(url, server_response, std::move(additional_args), use_tor, use_browser_useragent, err_msg == nullptr) != DownloadResult::OK) { - if(err_msg) - *err_msg = server_response; - return DownloadResult::NET_ERR; - } - - Json::CharReaderBuilder json_builder; - std::unique_ptr json_reader(json_builder.newCharReader()); - std::string json_errors; - if(!json_reader->parse(&server_response[0], &server_response[server_response.size()], &result, &json_errors)) { - fprintf(stderr, "download_json error: %s\n", json_errors.c_str()); - if(err_msg) - *err_msg = std::move(json_errors); - return DownloadResult::ERR; - } - - return DownloadResult::OK; - } + SuggestionResult download_result_to_suggestion_result(DownloadResult download_result) { return (SuggestionResult)download_result; } + PluginResult download_result_to_plugin_result(DownloadResult download_result) { return (PluginResult)download_result; } + SearchResult download_result_to_search_result(DownloadResult download_result) { return (SearchResult)download_result; } + ImageResult download_result_to_image_result(DownloadResult download_result) { return (ImageResult)download_result; } } \ No newline at end of file diff --git a/src/plugins/Pornhub.cpp b/src/plugins/Pornhub.cpp index 77d5594..afdd8fc 100644 --- a/src/plugins/Pornhub.cpp +++ b/src/plugins/Pornhub.cpp @@ -1,4 +1,5 @@ #include "../../plugins/Pornhub.hpp" +#include "../../include/StringUtils.hpp" #include #include @@ -11,14 +12,13 @@ namespace QuickMedia { return strstr(str, substr); } - // TODO: Speed this up by using string.find instead of parsing html - SuggestionResult Pornhub::update_search_suggestions(const std::string &text, BodyItems &result_items) { + SearchResult PornhubSearchPage::search(const std::string &str, BodyItems &result_items) { std::string url = "https://www.pornhub.com/video/search?search="; - url += url_param_encode(text); + url += url_param_encode(str); std::string website_data; - if(download_to_string(url, website_data, {}, use_tor) != DownloadResult::OK) - return SuggestionResult::NET_ERR; + if(download_to_string(url, website_data, {}, is_tor_enabled()) != DownloadResult::OK) + return SearchResult::NET_ERR; struct ItemData { BodyItems *result_items; @@ -65,14 +65,21 @@ namespace QuickMedia { cleanup: quickmedia_html_search_deinit(&html_search); - return result == 0 ? SuggestionResult::OK : SuggestionResult::ERR; + return result == 0 ? SearchResult::OK : SearchResult::ERR; } - BodyItems Pornhub::get_related_media(const std::string &url) { + PluginResult PornhubSearchPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + (void)title; + (void)url; + result_tabs.push_back(Tab{create_body(), std::make_unique(program), nullptr}); + return PluginResult::OK; + } + + BodyItems PornhubVideoPage::get_related_media(const std::string &url) { BodyItems result_items; std::string website_data; - if(download_to_string(url, website_data, {}, use_tor) != DownloadResult::OK) + if(download_to_string(url, website_data, {}, is_tor_enabled()) != DownloadResult::OK) return result_items; struct ItemData { diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index 7e1fc63..40b296d 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -1,22 +1,9 @@ #include "../../plugins/Youtube.hpp" #include "../../include/Storage.hpp" -#include -#include #include #include namespace QuickMedia { - static void iterate_suggestion_result(const Json::Value &value, std::vector &result_items, int &iterate_count) { - ++iterate_count; - if(value.isArray()) { - for(const Json::Value &child : value) { - iterate_suggestion_result(child, result_items, iterate_count); - } - } else if(value.isString() && iterate_count > 2) { - result_items.push_back(value.asString()); - } - } - static std::shared_ptr parse_content_video_renderer(const Json::Value &content_item_json, std::unordered_set &added_videos) { if(!content_item_json.isObject()) return nullptr; @@ -85,135 +72,6 @@ namespace QuickMedia { return body_item; } - Youtube::Youtube() : Plugin("youtube") { - - } - - PluginResult Youtube::get_front_page(BodyItems &result_items) { - bool disabled = true; - if(disabled) - return PluginResult::OK; - - std::string url = "https://youtube.com/"; - - std::vector additional_args = { - { "-H", "x-spf-referer: " + url }, - { "-H", "x-youtube-client-name: 1" }, - { "-H", "x-youtube-client-version: 2.20200626.03.00" }, - { "-H", "referer: " + url } - }; - - //std::vector cookies = get_cookies(); - //additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); - - Json::Value json_root; - DownloadResult result = download_json(json_root, url + "?pbj=1", std::move(additional_args), true); - if(result != DownloadResult::OK) return download_result_to_plugin_result(result); - - if(!json_root.isArray()) - return PluginResult::ERR; - - std::unordered_set added_videos; - - for(const Json::Value &json_item : json_root) { - if(!json_item.isObject()) - continue; - - const Json::Value &response_json = json_item["response"]; - if(!response_json.isObject()) - continue; - - const Json::Value &contents_json = response_json["contents"]; - if(!contents_json.isObject()) - continue; - - const Json::Value &tcbrr_json = contents_json["twoColumnBrowseResultsRenderer"]; - if(!tcbrr_json.isObject()) - continue; - - const Json::Value &tabs_json = tcbrr_json["tabs"]; - if(!tabs_json.isArray()) - continue; - - for(const Json::Value &tab_item_json : tabs_json) { - if(!tab_item_json.isObject()) - continue; - - const Json::Value &tab_renderer_json = tab_item_json["tabRenderer"]; - if(!tab_renderer_json.isObject()) - continue; - - const Json::Value &content_json = tab_renderer_json["content"]; - if(!content_json.isObject()) - continue; - - const Json::Value &rich_grid_renderer = content_json["richGridRenderer"]; - if(!rich_grid_renderer.isObject()) - continue; - - const Json::Value &contents2_json = rich_grid_renderer["contents"]; - if(!contents2_json.isArray()) - continue; - - for(const Json::Value &contents_item : contents2_json) { - const Json::Value &rich_item_renderer_json = contents_item["richItemRenderer"]; - if(!rich_item_renderer_json.isObject()) - continue; - - const Json::Value &rich_item_contents = rich_item_renderer_json["content"]; - std::shared_ptr body_item = parse_content_video_renderer(rich_item_contents, added_videos); - if(body_item) - result_items.push_back(std::move(body_item)); - } - } - } - - return PluginResult::OK; - } - - std::string Youtube::autocomplete_search(const std::string &query) { - // Return the last result if the query is a substring of the autocomplete result - if(last_autocomplete_result.size() >= query.size() && memcmp(query.data(), last_autocomplete_result.data(), query.size()) == 0) - return last_autocomplete_result; - - std::string url = "https://clients1.google.com/complete/search?client=youtube&hl=en&gs_rn=64&gs_ri=youtube&ds=yt&cp=7&gs_id=x&q="; - url += url_param_encode(query); - - std::string server_response; - if(download_to_string(url, server_response, {}, use_tor, true) != DownloadResult::OK) - return query; - - size_t json_start = server_response.find_first_of('('); - if(json_start == std::string::npos) - return query; - ++json_start; - - size_t json_end = server_response.find_last_of(')'); - if(json_end == std::string::npos) - return query; - - if(json_end == 0 || json_start >= json_end) - return query; - - Json::Value json_root; - Json::CharReaderBuilder json_builder; - std::unique_ptr json_reader(json_builder.newCharReader()); - std::string json_errors; - if(!json_reader->parse(&server_response[json_start], &server_response[json_end], &json_root, &json_errors)) { - fprintf(stderr, "Youtube autocomplete search json error: %s\n", json_errors.c_str()); - return query; - } - - int iterate_count = 0; - std::vector result_items; - iterate_suggestion_result(json_root, result_items, iterate_count); - if(result_items.empty()) - return query; - - last_autocomplete_result = result_items[0]; - return result_items[0]; - } - // Returns empty string if continuation token can't be found static std::string item_section_renderer_get_continuation_token(const Json::Value &item_section_renderer_json) { const Json::Value &continuations_json = item_section_renderer_json["continuations"]; @@ -277,9 +135,77 @@ namespace QuickMedia { } } - SuggestionResult Youtube::update_search_suggestions(const std::string &text, BodyItems &result_items) { + static std::string remove_index_from_playlist_url(const std::string &url) { + std::string result = url; + size_t index = result.rfind("&index="); + if(index == std::string::npos) + return result; + return result.substr(0, index); + } + + static std::shared_ptr parse_compact_video_renderer_json(const Json::Value &item_json, std::unordered_set &added_videos) { + const Json::Value &compact_video_renderer_json = item_json["compactVideoRenderer"]; + if(!compact_video_renderer_json.isObject()) + return nullptr; + + const Json::Value &video_id_json = compact_video_renderer_json["videoId"]; + if(!video_id_json.isString()) + return nullptr; + + std::string video_id_str = video_id_json.asString(); + if(added_videos.find(video_id_str) != added_videos.end()) + return nullptr; + + std::string thumbnail_url = "https://img.youtube.com/vi/" + video_id_str + "/hqdefault.jpg"; + + const char *date = nullptr; + const Json::Value &published_time_text_json = compact_video_renderer_json["publishedTimeText"]; + if(published_time_text_json.isObject()) { + const Json::Value &text_json = published_time_text_json["simpleText"]; + if(text_json.isString()) + date = text_json.asCString(); + } + + const char *length = nullptr; + const Json::Value &length_text_json = compact_video_renderer_json["lengthText"]; + if(length_text_json.isObject()) { + const Json::Value &text_json = length_text_json["simpleText"]; + if(text_json.isString()) + length = text_json.asCString(); + } + + const char *title = nullptr; + const Json::Value &title_json = compact_video_renderer_json["title"]; + if(title_json.isObject()) { + const Json::Value &simple_text_json = title_json["simpleText"]; + if(simple_text_json.isString()) { + title = simple_text_json.asCString(); + } + } + + if(!title) + return nullptr; + + auto body_item = BodyItem::create(title); + /* TODO: Make date a different color */ + std::string date_str; + if(date) + date_str += date; + if(length) { + if(!date_str.empty()) + date_str += '\n'; + date_str += length; + } + body_item->set_description(std::move(date_str)); + body_item->url = "https://www.youtube.com/watch?v=" + video_id_str; + body_item->thumbnail_url = std::move(thumbnail_url); + added_videos.insert(video_id_str); + return body_item; + } + + SearchResult YoutubeSearchPage::search(const std::string &str, BodyItems &result_items) { std::string url = "https://youtube.com/results?search_query="; - url += url_param_encode(text); + url += url_param_encode(str); std::vector additional_args = { { "-H", "x-spf-referer: " + url }, @@ -292,11 +218,11 @@ namespace QuickMedia { //additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); Json::Value json_root; - DownloadResult result = download_json(json_root, url + "?pbj=1", std::move(additional_args), true); - if(result != DownloadResult::OK) return download_result_to_suggestion_result(result); + DownloadResult result = download_json(json_root, url + "&pbj=1", std::move(additional_args), true); + if(result != DownloadResult::OK) return download_result_to_search_result(result); if(!json_root.isArray()) - return SuggestionResult::ERR; + return SearchResult::ERR; std::string continuation_token; std::unordered_set added_videos; /* The input contains duplicates, filter them out! */ @@ -345,10 +271,17 @@ namespace QuickMedia { if(!continuation_token.empty()) search_suggestions_get_continuation(url, continuation_token, result_items); - return SuggestionResult::OK; + return SearchResult::OK; + } + + PluginResult YoutubeSearchPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + (void)title; + (void)url; + result_tabs.push_back(Tab{create_body(), std::make_unique(program), nullptr}); + return PluginResult::OK; } - void Youtube::search_suggestions_get_continuation(const std::string &url, const std::string &continuation_token, BodyItems &result_items) { + void YoutubeSearchPage::search_suggestions_get_continuation(const std::string &url, const std::string &continuation_token, BodyItems &result_items) { std::string next_url = url + "&pbj=1&ctoken=" + continuation_token; std::vector additional_args = { @@ -393,93 +326,9 @@ namespace QuickMedia { } } - std::vector Youtube::get_cookies() const { - if(use_tor) - return {}; - - Path cookies_filepath; - if(get_cookies_filepath(cookies_filepath, name) != 0) { - fprintf(stderr, "Warning: Failed to create youtube cookies file\n"); - return {}; - } - - return { - CommandArg{ "-b", cookies_filepath.data }, - CommandArg{ "-c", cookies_filepath.data } - }; - } - - static std::string remove_index_from_playlist_url(const std::string &url) { - std::string result = url; - size_t index = result.rfind("&index="); - if(index == std::string::npos) - return result; - return result.substr(0, index); - } - - static std::shared_ptr parse_compact_video_renderer_json(const Json::Value &item_json, std::unordered_set &added_videos) { - const Json::Value &compact_video_renderer_json = item_json["compactVideoRenderer"]; - if(!compact_video_renderer_json.isObject()) - return nullptr; - - const Json::Value &video_id_json = compact_video_renderer_json["videoId"]; - if(!video_id_json.isString()) - return nullptr; - - std::string video_id_str = video_id_json.asString(); - if(added_videos.find(video_id_str) != added_videos.end()) - return nullptr; - - std::string thumbnail_url = "https://img.youtube.com/vi/" + video_id_str + "/hqdefault.jpg"; - - const char *date = nullptr; - const Json::Value &published_time_text_json = compact_video_renderer_json["publishedTimeText"]; - if(published_time_text_json.isObject()) { - const Json::Value &text_json = published_time_text_json["simpleText"]; - if(text_json.isString()) - date = text_json.asCString(); - } - - const char *length = nullptr; - const Json::Value &length_text_json = compact_video_renderer_json["lengthText"]; - if(length_text_json.isObject()) { - const Json::Value &text_json = length_text_json["simpleText"]; - if(text_json.isString()) - length = text_json.asCString(); - } - - const char *title = nullptr; - const Json::Value &title_json = compact_video_renderer_json["title"]; - if(title_json.isObject()) { - const Json::Value &simple_text_json = title_json["simpleText"]; - if(simple_text_json.isString()) { - title = simple_text_json.asCString(); - } - } - - if(!title) - return nullptr; - - auto body_item = BodyItem::create(title); - /* TODO: Make date a different color */ - std::string date_str; - if(date) - date_str += date; - if(length) { - if(!date_str.empty()) - date_str += '\n'; - date_str += length; - } - body_item->set_description(std::move(date_str)); - body_item->url = "https://www.youtube.com/watch?v=" + video_id_str; - body_item->thumbnail_url = std::move(thumbnail_url); - added_videos.insert(video_id_str); - return body_item; - } - // TODO: Make this faster by using string search instead of parsing html. // TODO: If the result is a play - BodyItems Youtube::get_related_media(const std::string &url) { + BodyItems YoutubeVideoPage::get_related_media(const std::string &url) { BodyItems result_items; std::string modified_url = remove_index_from_playlist_url(url); -- cgit v1.2.3