From cdf8d103f1ed6a932eb30b589b578d23ca66a514 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 10 May 2021 18:49:44 +0200 Subject: Add downloader, fix room navigation lag Fix bug where getting next page fails if there is no search bar --- src/Body.cpp | 12 +- src/DownloadUtils.cpp | 23 +- src/NetUtils.cpp | 28 ++ src/QuickMedia.cpp | 663 +++++++++++++++++++++++++++++++++++++++---- src/Storage.cpp | 18 ++ src/StringUtils.cpp | 10 + src/VideoPlayer.cpp | 3 +- src/main.cpp | 4 - src/plugins/FileManager.cpp | 36 ++- src/plugins/MangaGeneric.cpp | 30 +- 10 files changed, 709 insertions(+), 118 deletions(-) (limited to 'src') diff --git a/src/Body.cpp b/src/Body.cpp index 98f08e8..9494e80 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -400,7 +400,7 @@ namespace QuickMedia { } bool Body::on_event(const sf::RenderWindow &window, const sf::Event &event, bool keyboard_navigation) { - if(keyboard_navigation && event.type == sf::Event::KeyPressed) { + if(keyboard_navigation && event.type == sf::Event::KeyPressed && !event.key.alt) { if(event.key.code == sf::Keyboard::Up || (event.key.control && event.key.code == sf::Keyboard::K)) { bool top_reached = select_previous_item(); if(!top_reached && on_top_reached) @@ -1323,16 +1323,6 @@ namespace QuickMedia { return spacing_y; } - static size_t str_find_case_insensitive(const std::string &str, size_t start_index, const char *substr, size_t substr_len) { - auto it = std::search(str.begin() + start_index, str.end(), substr, substr + substr_len, - [](char c1, char c2) { - return std::toupper(c1) == std::toupper(c2); - }); - if(it == str.end()) - return std::string::npos; - return it - str.begin(); - } - // TODO: Support utf-8 case insensitive find static bool string_find_fuzzy_case_insensitive(const std::string &str, const std::string &substr) { if(str.empty()) return false; diff --git a/src/DownloadUtils.cpp b/src/DownloadUtils.cpp index 4afba38..53f8bb0 100644 --- a/src/DownloadUtils.cpp +++ b/src/DownloadUtils.cpp @@ -2,7 +2,10 @@ #include "../include/Program.hpp" #include "../include/Storage.hpp" #include "../external/cppcodec/base64_url.hpp" +#include +#include #include +#include #include #include @@ -54,7 +57,8 @@ namespace QuickMedia { args.push_back("-f"); for(const CommandArg &arg : additional_args) { args.push_back(arg.option.c_str()); - args.push_back(arg.value.c_str()); + if(!arg.value.empty()) + args.push_back(arg.value.c_str()); } if(use_browser_useragent) { args.push_back("-H"); @@ -137,6 +141,20 @@ namespace QuickMedia { return DownloadResult::OK; } + bool download_async_gui(const std::string &url, bool use_youtube_dl, bool no_video) { + char quickmedia_path[PATH_MAX]; + if(readlink("/proc/self/exe", quickmedia_path, sizeof(quickmedia_path)) == -1) + return false; + + std::vector args = { quickmedia_path, "download", "-u", url.c_str() }; + if(use_youtube_dl) + args.push_back("--youtube-dl"); + if(no_video) + args.push_back("--no-video"); + args.push_back(nullptr); + return exec_program_async(args.data(), nullptr) == 0; + } + // TODO: Add timeout DownloadResult download_to_json(const std::string &url, rapidjson::Document &result, const std::vector &additional_args, bool use_browser_useragent, bool fail_on_error) { sf::Clock timer; @@ -146,7 +164,8 @@ namespace QuickMedia { args.push_back("-f"); for(const CommandArg &arg : additional_args) { args.push_back(arg.option.c_str()); - args.push_back(arg.value.c_str()); + if(!arg.value.empty()) + args.push_back(arg.value.c_str()); } if(use_browser_useragent) { args.push_back("-H"); diff --git a/src/NetUtils.cpp b/src/NetUtils.cpp index 3539d46..dc7c2d2 100644 --- a/src/NetUtils.cpp +++ b/src/NetUtils.cpp @@ -4,6 +4,7 @@ #include #include #include +#include #include namespace QuickMedia { @@ -1742,4 +1743,31 @@ namespace QuickMedia { ++num_codepoints; } } + + std::string header_extract_value(const std::string &header, const std::string &type) { + std::string result; + string_split(header, '\n', [&type, &result](const char *str, size_t size) { + while(size > 0 && (*str == ' ' || *str == '\t')) { ++str; --size; } + if(size < type.size() || strncasecmp(str, type.c_str(), type.size()) != 0 || size == type.size()) + return true; + + str += type.size(); + size -= type.size(); + + const void *colon_ptr = memchr(str, ':', size); + if(!colon_ptr) + return true; + + const size_t colon_offset = (const char*)colon_ptr - str; + str += (colon_offset + 1); + size -= (colon_offset + 1); + + while(size > 0 && (*str == ' ' || *str == '\t')) { ++str; --size; } + while(size > 0 && (str[size - 1] == ' ' || str[size - 1] == '\t' || str[size - 1] == '\r' || str[size - 1] == '\n')) { --size; } + + result.assign(str, size); + return false; + }); + return result; + } } \ No newline at end of file diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index 112ec77..92c259e 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -36,6 +36,8 @@ #include #include #include +#include +#include #include #include @@ -78,7 +80,8 @@ static const std::pair valid_plugins[] = { std::make_pair("pleroma", "pleroma_logo.png"), std::make_pair("file-manager", nullptr), std::make_pair("stdin", nullptr), - std::make_pair("saucenao", nullptr) + std::make_pair("saucenao", nullptr), + std::make_pair("download", nullptr) }; static const char* get_plugin_logo_name(const char *plugin_name) { @@ -110,7 +113,7 @@ static const XRRModeInfo* get_mode_info(const XRRScreenResources *sr, RRMode id) return nullptr; } -static void for_each_active_monitor_output(Display *display, std::function callback_func) { +static void for_each_active_monitor_output(Display *display, std::function callback_func) { XRRScreenResources *screen_res = XRRGetScreenResources(display, DefaultRootWindow(display)); if(!screen_res) return; @@ -122,7 +125,7 @@ static void for_each_active_monitor_output(Display *display, std::functionmode); if(mode_info) - callback_func(mode_info); + callback_func(crt_info, mode_info); XRRFreeCrtcInfo(crt_info); } } @@ -135,7 +138,7 @@ static void for_each_active_monitor_output(Display *display, std::functionhTotal * mode_info->vTotal; if(total > 0) max_hz = std::max(max_hz, (unsigned long)std::round((double)mode_info->dotClock / (double)total)); @@ -148,9 +151,9 @@ static int get_monitor_max_hz(Display *display) { static int get_largest_monitor_height(Display *display) { int max_height = 0; - for_each_active_monitor_output(display, [&max_height](const XRRModeInfo *mode_info) { + for_each_active_monitor_output(display, [&max_height](const XRRCrtcInfo *crtc_info, const XRRModeInfo*) { // Need to get the min of width or height because we want to get the smallest size for monitors in portrait mode, for mobile devices such as pinephone - int width_or_height = std::min((int)mode_info->width, (int)mode_info->height); + int width_or_height = std::min((int)crtc_info->width, (int)crtc_info->height); max_height = std::max(max_height, width_or_height); }); @@ -350,6 +353,9 @@ namespace QuickMedia { const char *start_dir = nullptr; Window parent_window = None; std::vector tabs; + const char *url = nullptr; + bool download_use_youtube_dl = false; + std::string program_path = dirname(argv[0]); for(int i = 1; i < argc; ++i) { if(!plugin_name) { @@ -380,6 +386,17 @@ namespace QuickMedia { } } else if(strcmp(argv[i], "--low-cpu-mode") == 0) { low_cpu_mode = true; + } else if(strcmp(argv[i], "--youtube-dl") == 0) { + download_use_youtube_dl = true; + } else if(strcmp(argv[i], "-u") == 0) { + if(i < argc - 1) { + url = argv[i + 1]; + ++i; + } else { + fprintf(stderr, "Missing url after -u argument\n"); + usage(); + return -1; + } } else if(strcmp(argv[i], "-e") == 0) { if(i < argc - 1) { parent_window = strtol(argv[i + 1], nullptr, 0); @@ -468,18 +485,30 @@ namespace QuickMedia { FileManagerMimeType fm_mine_type = FILE_MANAGER_MIME_TYPE_ALL; FileSelectionHandler file_selection_handler = nullptr; - FileSelectionHandler saucenao_file_selection_handler = [this]() { + FileSelectionHandler saucenao_file_selection_handler = [this](FileManagerPage*, const std::filesystem::path&) { std::vector tabs; tabs.push_back(Tab{create_body(), std::make_unique(this, selected_files[0], true), nullptr}); return tabs; }; - init(parent_window); + init(parent_window, program_path); + + if(strcmp(plugin_name, "download") == 0) { + if(!url) { + fprintf(stderr, "-u argument has to be set when using the download plugin\n"); + usage(); + return -1; + } + download_page(url, download_use_youtube_dl); + return exit_code; + } + if(strcmp(plugin_name, "saucenao") == 0) { plugin_name = "file-manager"; fm_mine_type = FILE_MANAGER_MIME_TYPE_IMAGE; file_selection_handler = std::move(saucenao_file_selection_handler); } + load_plugin_by_name(tabs, start_dir, start_tab_index, fm_mine_type, std::move(file_selection_handler)); while(!tabs.empty() || matrix) { @@ -512,7 +541,23 @@ namespace QuickMedia { return exit_code; } - void Program::init(Window parent_window) { + static sf::Vector2i get_focused_monitor_center(Display *disp) { + int screen = DefaultScreen(disp); + int screen_center_x = DisplayWidth(disp, screen) / 2; + int screen_center_y = DisplayHeight(disp, screen) / 2; + + sf::Vector2i focused_monitor_center(screen_center_x, screen_center_y); + auto mouse_pos = sf::Mouse::getPosition(); + for_each_active_monitor_output(disp, [&focused_monitor_center, mouse_pos](const XRRCrtcInfo *crtc_info, const XRRModeInfo*){ + if(sf::Rect(crtc_info->x, crtc_info->y, crtc_info->width, crtc_info->height).contains(mouse_pos)) + focused_monitor_center = sf::Vector2i(crtc_info->x + crtc_info->width/2, crtc_info->y + crtc_info->height/2); + }); + + return focused_monitor_center; + } + + void Program::init(Window parent_window, std::string &program_path) { + XInitThreads(); disp = XOpenDisplay(NULL); if (!disp) throw std::runtime_error("Failed to open display to X11 server"); @@ -520,11 +565,10 @@ namespace QuickMedia { wm_delete_window_atom = XInternAtom(disp, "WM_DELETE_WINDOW", False); int screen = DefaultScreen(disp); - int screen_center_x = (DisplayWidth(disp, screen) - window_size.x) / 2; - int screen_center_y = (DisplayHeight(disp, screen) - window_size.y) / 2; + sf::Vector2i focused_monitor_center = get_focused_monitor_center(disp); x11_window = XCreateWindow(disp, parent_window ? parent_window : DefaultRootWindow(disp), - screen_center_x, screen_center_y, window_size.x, window_size.y, 0, + focused_monitor_center.x - window_size.x * 0.5f, focused_monitor_center.y - window_size.y * 0.5f, window_size.x, window_size.y, 0, DefaultDepth(disp, screen), InputOutput, DefaultVisual(disp, screen), @@ -532,15 +576,35 @@ namespace QuickMedia { if(!x11_window) throw std::runtime_error("Failed to create window"); + if(strcmp(plugin_name, "download") == 0) { + XSizeHints *size_hints = XAllocSizeHints(); + if(size_hints) { + size_hints->width = window_size.x; + size_hints->min_width = window_size.x; + size_hints->max_width = window_size.x; + + size_hints->height = window_size.y; + size_hints->min_height = window_size.y; + size_hints->max_height = window_size.y; + size_hints->flags = PSize | PMinSize | PMaxSize; + + XSetWMNormalHints(disp, x11_window, size_hints); + XFree(size_hints); + } + } + XStoreName(disp, x11_window, "QuickMedia"); XMapWindow(disp, x11_window); XFlush(disp); window.create(x11_window); + if(program_path.back() != '/') + program_path += '/'; + resources_root = "/usr/share/quickmedia/"; - if(get_file_type("../../../images/manganelo_logo.png") == FileType::REGULAR) { - resources_root = "../../../"; + if(get_file_type(program_path + "../../../images/manganelo_logo.png") == FileType::REGULAR) { + resources_root = program_path + "../../../"; } set_resource_loader_root_path(resources_root.c_str()); @@ -1430,7 +1494,9 @@ namespace QuickMedia { std::function submit_handler = [this, &submit_handler, &after_submit_handler, &tabs, &tab_associated_data, &ui_tabs, &loop_running, &redraw](const std::string &search_text) { const int selected_tab = ui_tabs.get_selected(); auto selected_item = tabs[selected_tab].body->get_selected_shared(); - if(!selected_item && !tabs[selected_tab].page->allow_submit_no_selection()) + if(tabs[selected_tab].page->allow_submit_no_selection() && (sf::Keyboard::isKeyPressed(sf::Keyboard::LControl) || sf::Keyboard::isKeyPressed(sf::Keyboard::RControl))) + selected_item = nullptr; + if(!selected_item && (!tabs[selected_tab].page->allow_submit_no_selection() || search_text.empty())) return; hide_virtual_keyboard(); @@ -1604,38 +1670,38 @@ namespace QuickMedia { for(size_t i = 0; i < tabs.size(); ++i) { Tab &tab = tabs[i]; + tab.body->body_item_select_callback = [&submit_handler](BodyItem *body_item) { submit_handler(body_item->get_title()); }; - TabAssociatedData &associated_data = tab_associated_data[i]; - if(!tab.search_bar) - continue; - - // 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.body->on_bottom_reached = on_bottom_reached; - tab.search_bar->onTextSubmitCallback = [&submit_handler, &associated_data](const std::string &search_text) { - if(associated_data.typing) - return; - submit_handler(search_text); - }; + TabAssociatedData &associated_data = tab_associated_data[i]; + if(tab.search_bar) { + // 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.body->on_bottom_reached = on_bottom_reached; + tab.search_bar->onTextSubmitCallback = [&submit_handler, &associated_data](const std::string &search_text) { + if(associated_data.typing) + return; + submit_handler(search_text); + }; + } } sf::Event event; @@ -2203,6 +2269,8 @@ namespace QuickMedia { current_page = previous_page; } else if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::C && event.key.control) { save_video_url_to_clipboard(); + } else if(event.type == sf::Event::KeyPressed && event.key.code == sf::Keyboard::S && event.key.control) { + download_async_gui(original_video_url, true, force_no_video); } } handle_window_close(); @@ -2218,6 +2286,8 @@ namespace QuickMedia { break; } else if(pressed_keysym == XK_f && pressing_ctrl) { window_set_fullscreen(disp, window.getSystemHandle(), WindowFullscreenState::TOGGLE); + } else if(pressed_keysym == XK_s && pressing_ctrl) { + download_async_gui(original_video_url, true, force_no_video); } else if(pressed_keysym == XK_r && pressing_ctrl) { if(!cursor_visible) window.setMouseCursorVisible(true); @@ -3112,7 +3182,7 @@ namespace QuickMedia { } else if(event.key.code == sf::Keyboard::P) { BodyItem *selected_item = thread_body->get_selected(); if(selected_item && !selected_item->url.empty()) { - if(is_url_video(selected_item->url)) { + if(is_url_video(selected_item->url)) { page_stack.push(PageType::IMAGE_BOARD_THREAD); current_page = PageType::VIDEO_CONTENT; watched_videos.clear(); @@ -3298,6 +3368,8 @@ namespace QuickMedia { page_loop(saucenao_tabs); redraw = true; frame_skip_text_entry = true; + } else if(event.key.code == sf::Keyboard::S && event.key.control) { + download_async_gui(attached_image_url, false, false); } } } @@ -4505,15 +4577,6 @@ namespace QuickMedia { } }; - if(!matrix->is_initial_sync_finished()) { - previous_messages_future = AsyncTask([this, ¤t_room]() { - Messages messages; - if(matrix->get_previous_room_messages(current_room, messages, true) != PluginResult::OK) - fprintf(stderr, "Failed to get previous matrix messages in room: %s\n", current_room->id.c_str()); - return messages; - }); - } - sf::RectangleShape more_messages_below_rect; more_messages_below_rect.setFillColor(sf::Color(128, 50, 50)); @@ -5546,4 +5609,502 @@ namespace QuickMedia { matrix->stop_sync(); } + + enum class DownloadUpdateStatus { + DOWNLOADING, + FINISHED, + ERROR + }; + + class Downloader { + public: + Downloader(const std::string &url, const std::string &output_filepath) : url(url), output_filepath(output_filepath) {} + virtual ~Downloader() = default; + + virtual bool start() = 0; + virtual void stop(bool download_completed) = 0; + virtual DownloadUpdateStatus update() = 0; + + virtual float get_progress() = 0; + virtual std::string get_progress_text() = 0; + virtual std::string get_download_speed_text() = 0; + protected: + std::string url; + std::string output_filepath; + }; + + class CurlDownloader : public Downloader { + public: + CurlDownloader(const std::string &url, const std::string &output_filepath) : Downloader(url, output_filepath) { + output_filepath_tmp = output_filepath; + output_filepath_tmp.append(".tmp"); + read_program.pid = -1; + read_program.read_fd = -1; + progress_text = "0 bytes/Unknown"; + download_speed_text = "Unknown/s"; + } + + bool start() override { + remove(output_filepath.c_str()); + remove(output_filepath_tmp.data.c_str()); + + const char *args[] = { "curl", + "-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", "--compressed", + "-H", "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36", + "-g", "-s", "-L", "-f", "-o", output_filepath_tmp.data.c_str(), + "-D", "/dev/stdout", + "--", url.c_str(), nullptr }; + + if(exec_program_pipe(args, &read_program) != 0) + return false; + + header_reader = AsyncTask([this]{ + char tmp_buf[1024]; + while(true) { + ssize_t bytes_available = read(read_program.read_fd, tmp_buf, sizeof(tmp_buf)); + if(bytes_available == -1) { + return false; + } else if(bytes_available > 0 && content_length == (size_t)-1) { + header.append(tmp_buf, bytes_available); + if(header.find("\r\n\r\n") != std::string::npos) { + std::string content_length_str = header_extract_value(header, "content-length"); + if(!content_length_str.empty()) { + errno = 0; + char *endptr; + const long content_length_tmp = strtol(content_length_str.c_str(), &endptr, 10); + if(endptr != content_length_str.c_str() && errno == 0) { + std::lock_guard lock(content_length_mutex); + content_length = content_length_tmp; + } + } + } + } + } + return true; + }); + + return true; + } + + void stop(bool download_completed) override { + if(read_program.pid != -1) + close(read_program.pid); + if(read_program.read_fd != -1) + kill(read_program.read_fd, SIGTERM); + if(!download_completed) + remove(output_filepath_tmp.data.c_str()); + //header_reader.cancel(); + } + + DownloadUpdateStatus update() override { + int status = 0; + if(wait_program_non_blocking(read_program.pid, &status)) { + read_program.pid = -1; + if(status == 0 && rename_atomic(output_filepath_tmp.data.c_str(), output_filepath.c_str()) == 0) { + return DownloadUpdateStatus::FINISHED; + } else { + return DownloadUpdateStatus::ERROR; + } + } + + if(header_reader.ready()) { + if(!header_reader.get()) + return DownloadUpdateStatus::ERROR; + } + + std::lock_guard lock(content_length_mutex); + size_t output_file_size = 0; + file_get_size(output_filepath_tmp, &output_file_size); + size_t downloaded_size = std::min(output_file_size, content_length); + + if(content_length == (size_t)-1) { + progress_text = std::to_string(output_file_size / 1024) + "/Unknown"; + } else { + size_t percentage = 0; + if(output_file_size > 0) + percentage = (double)downloaded_size / (double)content_length * 100.0; + progress = (double)percentage / 100.0; + progress_text = file_size_to_human_readable_string(downloaded_size) + "/" + file_size_to_human_readable_string(content_length) + " (" + std::to_string(percentage) + "%)"; + } + + // TODO: Take into consideration time overflow? + size_t downloaded_diff = std::max(0lu, downloaded_size - downloaded_since_last_check); + download_speed_text = file_size_to_human_readable_string(downloaded_diff) + "/s"; + downloaded_since_last_check = downloaded_size; + + return DownloadUpdateStatus::DOWNLOADING; + } + + float get_progress() override { + return progress; + } + + std::string get_progress_text() override { + return progress_text; + } + + std::string get_download_speed_text() override { + return download_speed_text; + } + private: + Path output_filepath_tmp; + ReadProgram read_program; + AsyncTask header_reader; + std::string header; + size_t content_length = (size_t)-1; + size_t downloaded_since_last_check = 0; + float progress = 0.0f; + std::mutex content_length_mutex; + std::string progress_text; + std::string download_speed_text; + }; + + class YoutubeDlDownloader : public Downloader { + public: + YoutubeDlDownloader(const std::string &url, const std::string &output_filepath, bool no_video) : Downloader(url, output_filepath), no_video(no_video) { + // youtube-dl requires a file extension for the file + if(this->output_filepath.find('.') == std::string::npos) + this->output_filepath += ".mkv"; + + read_program.pid = -1; + read_program.read_fd = -1; + progress_text = "0.0% of Unknown"; + download_speed_text = "Unknown/s"; + } + + bool start() override { + remove(output_filepath.c_str()); + + std::vector args = { "youtube-dl", "--no-warnings", "--no-continue", "--output", output_filepath.c_str(), "--newline" }; + if(no_video) + args.push_back("-x"); + args.insert(args.end(), { "--", url.c_str(), nullptr }); + + if(exec_program_pipe(args.data(), &read_program) != 0) + return false; + + read_program_file = fdopen(read_program.read_fd, "rb"); + if(!read_program_file) { + wait_program(read_program.pid); + return false; + } + + youtube_dl_output_reader = AsyncTask([this]{ + char line[128]; + char progress_c[10]; + char content_size_c[20]; + char download_speed_c[20]; + + while(true) { + if(fgets(line, sizeof(line), read_program_file)) { + int len = strlen(line); + if(len > 0 && line[len - 1] == '\n') { + line[len - 1] = '\0'; + --len; + } + + if(sscanf(line, "[download] %10s of %20s at %20s", progress_c, content_size_c, download_speed_c) == 3) { + std::lock_guard lock(progress_update_mutex); + + if(strcmp(progress_c, "Unknown") != 0 && strcmp(content_size_c, "Unknown") != 0) { + std::string progress_str = progress_c; + progress_text = progress_str + " of " + content_size_c; + if(progress_str.back() == '%') { + errno = 0; + char *endptr; + const double progress_tmp = strtod(progress_str.c_str(), &endptr); + if(endptr != progress_str.c_str() && errno == 0) + progress = progress_tmp / 100.0; + } + } + + if(strcmp(download_speed_c, "Unknown") == 0) + download_speed_text = "Unknown/s"; + else + download_speed_text = download_speed_c; + } + } else { + return false; + } + } + + return true; + }); + + return true; + } + + void stop(bool) override { + if(read_program_file) + fclose(read_program_file); + if(read_program.read_fd != -1) + kill(read_program.read_fd, SIGTERM); + // TODO: Remove the temporary files created by youtube-dl (if !download_completed) + //header_reader.cancel(); + } + + DownloadUpdateStatus update() override { + int status = 0; + if(wait_program_non_blocking(read_program.pid, &status)) { + read_program.pid = -1; + if(status == 0) { + return DownloadUpdateStatus::FINISHED; + } else { + return DownloadUpdateStatus::ERROR; + } + } + + if(youtube_dl_output_reader.ready()) { + if(!youtube_dl_output_reader.get()) + return DownloadUpdateStatus::ERROR; + } + + return DownloadUpdateStatus::DOWNLOADING; + } + + float get_progress() override { + std::lock_guard lock(progress_update_mutex); + return progress; + } + + std::string get_progress_text() override { + std::lock_guard lock(progress_update_mutex); + return progress_text; + } + + std::string get_download_speed_text() override { + std::lock_guard lock(progress_update_mutex); + return download_speed_text; + } + private: + ReadProgram read_program; + FILE *read_program_file = nullptr; + AsyncTask youtube_dl_output_reader; + std::mutex progress_update_mutex; + float progress = 0.0f; + std::string progress_text; + std::string download_speed_text; + bool no_video; + }; + + static const char* get_filename(const char *path) { + const char *p = (const char*)memrchr(path, '/', strlen(path)); + return p ? p + 1 : path; + } + + class ConfirmationPage : public Page { + public: + ConfirmationPage(Program *program, FileManagerPage *file_manager_page, bool *file_overwrite, const std::string &title) : Page(program), file_manager_page(file_manager_page), file_overwrite(file_overwrite), title(title) {} + const char* get_title() const override { return title.c_str(); } + PluginResult submit(const std::string &title, const std::string&, std::vector&) override { + if(title == "Yes") { + *file_overwrite = true; + file_manager_page->close = true; + } else { + *file_overwrite = false; + } + program->set_go_to_previous_page(); + return PluginResult::OK; + } + + static void add_items(BodyItems &items) { + items.push_back(BodyItem::create("No")); + items.push_back(BodyItem::create("Yes")); + } + private: + FileManagerPage *file_manager_page; + bool *file_overwrite; + std::string title; + }; + + void Program::download_page(const char *url, bool download_use_youtube_dl) { + bool file_overwrite = true; + FileSelectionHandler overwrite_confirm_handler = [this, &file_overwrite](FileManagerPage *file_manager_page, const std::filesystem::path &path) { + file_overwrite = true; + std::vector tabs; + if(std::filesystem::exists(path)) { + auto body = create_body(); + ConfirmationPage::add_items(body->items); + tabs.push_back(Tab{ std::move(body), std::make_unique(this, file_manager_page, &file_overwrite, "Are you sure you want to overwrite " + path.string() + "?"), nullptr }); + } + return tabs; + }; + + auto file_manager_page = std::make_unique(this, FILE_MANAGER_MIME_TYPE_ALL, std::move(overwrite_confirm_handler), true, "Where do you want to save the file? Current directory: "); + 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 file_manager_tabs; + file_manager_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(file_manager_tabs); + + if(!window.isOpen() || selected_files.empty() || !file_overwrite) { + exit_code = 1; + return; + } + + sf::Vector2i focused_monitor_center = get_focused_monitor_center(disp); + window_size.x = std::floor(300.0f + 380.0f * get_ui_scale()); + window_size.y = std::floor(50.0f + 130.0f * get_ui_scale()); + window.setSize(sf::Vector2u(window_size.x, window_size.y)); + XSizeHints *size_hints = XAllocSizeHints(); + if(size_hints) { + size_hints->width = window_size.x; + size_hints->min_width = window_size.x; + size_hints->max_width = window_size.x; + + size_hints->height = window_size.y; + size_hints->min_height = window_size.y; + size_hints->max_height = window_size.y; + size_hints->flags = PSize | PMinSize | PMaxSize; + + XSetWMNormalHints(disp, x11_window, size_hints); + XFree(size_hints); + XFlush(disp); + } + window.setPosition(sf::Vector2i(focused_monitor_center.x - window_size.x * 0.5f, focused_monitor_center.y - window_size.y * 0.5f)); + + std::string output_filepath = selected_files[0]; + std::string output_filepath_s = output_filepath; + char *output_dir = dirname(output_filepath_s.data()); + if(create_directory_recursive(output_dir) != 0) { + show_notification("QuickMedia", std::string("Failed to download ") + url + " to " + output_filepath, Urgency::CRITICAL); + exit_code = 1; + return; + } + + window.setFramerateLimit(monitor_hz); + window.clear(back_color); + window.display(); + + const float loading_bar_padding_x = std::floor(4.0f * get_ui_scale()); + const float loading_bar_padding_y = std::floor(4.0f * get_ui_scale()); + RoundedRectangle loading_bar_background(sf::Vector2f(1.0f, 1.0f), std::floor(10.0f * get_ui_scale()), sf::Color(21, 25, 30), &rounded_rectangle_shader); + RoundedRectangle loading_bar(sf::Vector2f(1.0f, 1.0f), std::floor(10.0f * get_ui_scale() - loading_bar_padding_y), sf::Color(0, 85, 119), &rounded_rectangle_shader); + + const float padding_x = std::floor(30.0f * get_ui_scale()); + const float spacing_y = std::floor(15.0f * get_ui_scale()); + const float loading_bar_height = std::floor(20.0f * get_ui_scale()); + + sf::Text progress_text("0kb/Unknown", *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(20.0f * get_ui_scale())); + sf::Text status_text("Downloading", *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(20.0f * get_ui_scale())); + sf::Text filename_text(get_filename(output_filepath.c_str()), *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(14.0f * get_ui_scale())); + filename_text.setFillColor(sf::Color(179, 179, 179)); + sf::Text download_speed_text("0 bytes/s", *FontLoader::get_font(FontLoader::FontType::LATIN), std::floor(14.0f * get_ui_scale())); + download_speed_text.setFillColor(sf::Color(179, 179, 179)); + + bool redraw = true; + sf::Event event; + + std::unique_ptr downloader; + if(download_use_youtube_dl) + downloader = std::make_unique(url, output_filepath, force_no_video); + else + downloader = std::make_unique(url, output_filepath); + + if(!downloader->start()) { + show_notification("QuickMedia", std::string("Failed to download ") + url + " to " + output_filepath, Urgency::CRITICAL); + exit_code = 1; + return; + } + + sf::Clock frame_timer; + sf::Clock progress_update_timer; + bool download_completed = false; + float progress = 0.0f; + float ui_progress = 0.0f; + + while(window.isOpen()) { + while (window.pollEvent(event)) { + 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)); + redraw = true; + } + } + + if(handle_window_close()) { + show_notification("QuickMedia", "Download cancelled!"); + downloader->stop(false); + exit_code = 1; + exit(exit_code); + } + + if(progress_update_timer.getElapsedTime().asSeconds() >= 1.0f) { + progress_update_timer.restart(); + DownloadUpdateStatus update_status = downloader->update(); + switch(update_status) { + case DownloadUpdateStatus::DOWNLOADING: + break; + case DownloadUpdateStatus::FINISHED: + download_completed = true; + goto cleanup; + case DownloadUpdateStatus::ERROR: + goto cleanup; + } + + progress = downloader->get_progress(); + progress = std::max(0.0f, std::min(1.0f, progress)); + progress_text.setString(downloader->get_progress_text()); + download_speed_text.setString(downloader->get_download_speed_text()); + redraw = true; + } + + if(redraw) { + redraw = false; + loading_bar_background.set_size(sf::Vector2f(window_size.x - padding_x * 2.0f, loading_bar_height)); + loading_bar_background.set_position(window_size * 0.5f - loading_bar_background.get_size() * 0.5f + sf::Vector2f(0.0f, download_speed_text.getLocalBounds().height * 0.5f)); + loading_bar_background.set_position(sf::Vector2f(std::floor(loading_bar_background.get_position().x), std::floor(loading_bar_background.get_position().y))); + loading_bar.set_position(loading_bar_background.get_position() + sf::Vector2f(loading_bar_padding_x, loading_bar_padding_y)); + filename_text.setPosition( + loading_bar_background.get_position() + sf::Vector2f(0.0f, -(filename_text.getLocalBounds().height + spacing_y))); + progress_text.setPosition( + filename_text.getPosition() + sf::Vector2f(loading_bar_background.get_size().x - progress_text.getLocalBounds().width, -(progress_text.getLocalBounds().height + spacing_y))); + status_text.setPosition( + filename_text.getPosition() + sf::Vector2f(0.0f, -(status_text.getLocalBounds().height + spacing_y))); + download_speed_text.setPosition( + loading_bar_background.get_position() + sf::Vector2f(0.0f, loading_bar_height + spacing_y)); + } + + const float progress_diff = std::abs(progress - ui_progress); + const float progress_move = frame_timer.getElapsedTime().asSeconds() * 500.0f * progress_diff; + if(progress_diff < progress_move) { + ui_progress = progress; + } else { + if(progress_diff > 0.0f) + ui_progress += progress_move; + else + ui_progress -= progress_move; + } + + loading_bar.set_size(sf::Vector2f( + std::floor((loading_bar_background.get_size().x - loading_bar_padding_x) * ui_progress), + loading_bar_height - loading_bar_padding_y * 2.0f)); + + window.clear(sf::Color(33, 37, 44)); + loading_bar_background.draw(window); + loading_bar.draw(window); + window.draw(progress_text); + window.draw(status_text); + window.draw(filename_text); + window.draw(download_speed_text); + window.display(); + frame_timer.restart(); + } + + cleanup: + downloader->stop(download_completed); + if(download_completed) { + show_notification("QuickMedia", std::string("Download finished! Downloaded ") + url + " to " + output_filepath); + exit_code = 0; + } else { + show_notification("QuickMedia", std::string("Failed to download ") + url + " to " + output_filepath, Urgency::CRITICAL); + exit_code = 1; + } + exit(exit_code); + } } diff --git a/src/Storage.cpp b/src/Storage.cpp index 6a98267..85a150c 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -259,4 +259,22 @@ namespace QuickMedia { return false; } + + std::string file_size_to_human_readable_string(size_t bytes) { + double kb = (double)bytes / 1024.0; + double mb = (double)bytes / 1024.0 / 1024.0; + double gb = (double)bytes / 1024.0 / 1024.0 / 1024.0; + char result[32]; + + if(gb >= 1.0) + snprintf(result, sizeof(result), "%.1f GiB", gb); + else if(mb >= 1.0) + snprintf(result, sizeof(result), "%.1f MiB", mb); + else if(kb >= 1.0) + snprintf(result, sizeof(result), "%.1f KiB", kb); + else + snprintf(result, sizeof(result), "%zu bytes", bytes); + + return result; + } } diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp index 84e2ce5..0345558 100644 --- a/src/StringUtils.cpp +++ b/src/StringUtils.cpp @@ -87,4 +87,14 @@ namespace QuickMedia { size_t ends_len = ends_with_str.size(); return ends_len == 0 || (str.size() >= ends_len && memcmp(&str[str.size() - ends_len], ends_with_str.data(), ends_len) == 0); } + + size_t str_find_case_insensitive(const std::string &str, size_t start_index, const char *substr, size_t substr_len) { + auto it = std::search(str.begin() + start_index, str.end(), substr, substr + substr_len, + [](char c1, char c2) { + return std::toupper(c1) == std::toupper(c2); + }); + if(it == str.end()) + return std::string::npos; + return it - str.begin(); + } } \ No newline at end of file diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp index 5f860d2..fd5ba79 100644 --- a/src/VideoPlayer.cpp +++ b/src/VideoPlayer.cpp @@ -208,9 +208,10 @@ namespace QuickMedia { Window parent_window; Window *child_window; unsigned int num_children; - if(XQueryTree(display, window, &root_window, &parent_window, &child_window, &num_children) != 0) { + if(XQueryTree(display, window, &root_window, &parent_window, &child_window, &num_children)) { for(unsigned int i = 0; i < num_children; i++) result.push_back(child_window[i]); + XFree(child_window); } return result; } diff --git a/src/main.cpp b/src/main.cpp index 3383363..89d824d 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,12 +1,8 @@ #include "../include/QuickMedia.hpp" -#include -#include #include int main(int argc, char **argv) { - chdir(dirname(argv[0])); setlocale(LC_ALL, "C"); // Sigh... stupid C - XInitThreads(); QuickMedia::Program program; return program.run(argc, argv); } diff --git a/src/plugins/FileManager.cpp b/src/plugins/FileManager.cpp index 52f9f4e..e1f3b04 100644 --- a/src/plugins/FileManager.cpp +++ b/src/plugins/FileManager.cpp @@ -23,24 +23,6 @@ namespace QuickMedia { return last_write_time; } - static std::string file_size_to_human_readable_string(size_t bytes) { - double kb = (double)bytes / 1024.0; - double mb = (double)bytes / 1024.0 / 1024.0; - double gb = (double)bytes / 1024.0 / 1024.0 / 1024.0; - char result[32]; - - if(gb >= 1.0) - snprintf(result, sizeof(result), "%.1f GiB", gb); - else if(mb >= 1.0) - snprintf(result, sizeof(result), "%.1f MiB", mb); - else if(kb >= 1.0) - snprintf(result, sizeof(result), "%.1f KiB", kb); - else - snprintf(result, sizeof(result), "%zu bytes", bytes); - - return result; - } - PluginResult FileManagerPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { (void)url; @@ -53,14 +35,22 @@ namespace QuickMedia { if(std::filesystem::is_regular_file(new_path)) { program->select_file(new_path); if(selection_handler) - result_tabs = selection_handler(); + result_tabs = selection_handler(this, new_path); return PluginResult::OK; } - if(!std::filesystem::is_directory(new_path)) + if(!std::filesystem::is_directory(new_path)) { + if(allow_empty_match_submit) { + program->select_file(new_path); + if(selection_handler) + result_tabs = selection_handler(this, new_path); + return PluginResult::OK; + } return PluginResult::ERR; + } current_dir = std::move(new_path); + this->title = title_prefix + current_dir.string(); BodyItems result_items; PluginResult result = get_files_in_directory(result_items); @@ -73,10 +63,16 @@ namespace QuickMedia { return PluginResult::OK; } + void FileManagerPage::on_navigate_to_page(Body*) { + if(close) + program->set_go_to_previous_page(); + } + bool FileManagerPage::set_current_directory(const std::string &path) { if(!std::filesystem::is_directory(path)) return false; current_dir = path; + title = title_prefix + current_dir.string(); return true; } diff --git a/src/plugins/MangaGeneric.cpp b/src/plugins/MangaGeneric.cpp index a6df1c1..2e88d60 100644 --- a/src/plugins/MangaGeneric.cpp +++ b/src/plugins/MangaGeneric.cpp @@ -121,34 +121,6 @@ namespace QuickMedia { }, page_image_userdata); } - static size_t str_find_case_insensitive(const std::string &str, size_t start_index, const char *substr, size_t substr_len) { - auto it = std::search(str.begin() + start_index, str.end(), substr, substr + substr_len, - [](char c1, char c2) { - return std::toupper(c1) == std::toupper(c2); - }); - if(it == str.end()) - return std::string::npos; - return it - str.begin(); - } - - static std::string header_extract_location(const std::string &headers) { - size_t index = str_find_case_insensitive(headers, 0, "location:", 9); - if(index != std::string::npos && (index == 0 || headers[index - 1] == '\n')) { - index += 9; - size_t end = headers.find('\r', index); - size_t start = index; - while(start < end) { - char c = headers[start]; - if(c != ' ' && c != '\t') - break; - ++start; - } - if(end - start > 0) - return headers.substr(start, end - start); - } - return ""; - } - MangaGenericSearchPage::MangaGenericSearchPage(Program *program, const char *service_name, const char *website_url, bool fail_on_http_error) : Page(program), service_name(service_name), website_url(website_url ? website_url : ""), fail_on_http_error(fail_on_http_error) { @@ -228,7 +200,7 @@ namespace QuickMedia { goto cleanup; } - target_url = header_extract_location(response_headers); + target_url = header_extract_value(response_headers, "location"); if(target_url.empty()) { fprintf(stderr, "Failed to extract target location from %s HEAD\n", url.c_str()); result = -1; -- cgit v1.2.3