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/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 ++++++---------------- 14 files changed, 951 insertions(+), 1139 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/plugins') 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