From 9799803529c57930a0e7f12e45cbcf2b2e4419eb Mon Sep 17 00:00:00 2001 From: dec05eba Date: Thu, 28 May 2020 02:08:40 +0200 Subject: Add support for mangadex --- README.md | 4 +- images/mangadex_logo.png | Bin 0 -> 4094 bytes include/Notification.hpp | 32 ++++ include/QuickMedia.hpp | 4 +- plugins/Manga.hpp | 19 +++ plugins/Mangadex.hpp | 31 ++++ plugins/Manganelo.hpp | 16 +- plugins/Plugin.hpp | 1 + src/Body.cpp | 1 + src/Program.c | 24 +++ src/QuickMedia.cpp | 133 ++++++---------- src/Storage.cpp | 8 +- src/plugins/Mangadex.cpp | 377 ++++++++++++++++++++++++++++++++++++++++++++++ src/plugins/Manganelo.cpp | 34 +++++ 14 files changed, 585 insertions(+), 99 deletions(-) create mode 100644 images/mangadex_logo.png create mode 100644 include/Notification.hpp create mode 100644 plugins/Manga.hpp create mode 100644 plugins/Mangadex.hpp create mode 100644 src/plugins/Mangadex.cpp diff --git a/README.md b/README.md index 35b57c5..ee5acc7 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # QuickMedia Native clients of websites with fast access to what you want to see, **with TOR support**. See [old video demo with manga](https://lbry.tv/quickmedia_manga-2019-08-05_21.20.46/7).\ -Currently supported websites: `youtube`, `manganelo`, `4chan` and _others_.\ +Currently supported websites: `youtube`, `manganelo`, `mangadex`, `4chan` and _others_.\ **Note:** Manganelo doesn't work when used with TOR.\ **Note:** Posting comments on 4chan doesn't work when used with TOR. However browing works.\ **Note:** TOR system service needs to be running (`systemctl start tor.service`).\ @@ -11,7 +11,7 @@ Config data, including manga progress is stored under `$HOME/.config/quickmedia` ``` usage: QuickMedia [--tor] OPTIONS: -plugin The plugin to use. Should be either 4chan, manganelo or youtube +plugin The plugin to use. Should be either 4chan, manganelo, mangadex or youtube --tor Use tor. Disabled by default EXAMPLES: QuickMedia manganelo diff --git a/images/mangadex_logo.png b/images/mangadex_logo.png new file mode 100644 index 0000000..698a879 Binary files /dev/null and b/images/mangadex_logo.png differ diff --git a/include/Notification.hpp b/include/Notification.hpp new file mode 100644 index 0000000..22f2f77 --- /dev/null +++ b/include/Notification.hpp @@ -0,0 +1,32 @@ +#pragma once + +#include "Program.h" +#include +#include + +namespace QuickMedia { + enum class Urgency { + LOW, + NORMAL, + CRITICAL + }; + + static const char* urgency_string(Urgency urgency) { + switch(urgency) { + case Urgency::LOW: + return "low"; + case Urgency::NORMAL: + return "normal"; + case Urgency::CRITICAL: + return "critical"; + } + assert(false); + return nullptr; + } + + static void show_notification(const std::string &title, const std::string &description, Urgency urgency = Urgency::NORMAL) { + const char *args[] = { "notify-send", "-u", urgency_string(urgency), "--", title.c_str(), description.c_str(), nullptr }; + exec_program_async(args, nullptr); + printf("Notification: title: %s, description: %s\n", title.c_str(), description.c_str()); + } +} \ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index bd72806..19d84a8 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -15,7 +15,7 @@ namespace QuickMedia { class Plugin; - class Manganelo; + class Manga; class Program { public: @@ -43,7 +43,7 @@ namespace QuickMedia { }; LoadImageResult load_image_by_index(int image_index, sf::Texture &image_texture, sf::String &error_message); - void download_chapter_images_if_needed(Manganelo *image_plugin); + void download_chapter_images_if_needed(Manga *image_plugin); void select_episode(BodyItem *item, bool start_from_beginning); // Returns Page::EXIT if empty diff --git a/plugins/Manga.hpp b/plugins/Manga.hpp new file mode 100644 index 0000000..18ed2f9 --- /dev/null +++ b/plugins/Manga.hpp @@ -0,0 +1,19 @@ +#pragma once + +#include "Plugin.hpp" +#include +#include + +namespace QuickMedia { + // Return false to stop iteration + using PageCallback = std::function; + + class Manga : public Plugin { + public: + Manga(const std::string &plugin_name) : Plugin(plugin_name) {} + bool is_manga() override { return true; } + virtual ImageResult get_number_of_images(const std::string &url, int &num_images) = 0; + virtual ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) = 0; + virtual bool extract_id_from_url(const std::string &url, std::string &manga_id) = 0; + }; +} \ No newline at end of file diff --git a/plugins/Mangadex.hpp b/plugins/Mangadex.hpp new file mode 100644 index 0000000..9d6d366 --- /dev/null +++ b/plugins/Mangadex.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include "Manga.hpp" +#include +#include + +namespace QuickMedia { + class Mangadex : public Manga { + public: + Mangadex() : Manga("mangadex") {} + SearchResult search(const std::string &url, BodyItems &result_items) override; + SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override; + ImageResult get_number_of_images(const std::string &url, int &num_images) override; + bool search_suggestions_has_thumbnails() const override { return true; } + bool search_results_has_thumbnails() const override { return false; } + int get_search_delay() const override { return 300; } + Page get_page_after_search() const override { return Page::EPISODE_LIST; } + + ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) override; + + bool extract_id_from_url(const std::string &url, std::string &manga_id) override; + private: + SearchResult search_page(const std::string &url, BodyItems &result_items, int page, bool *is_last_page); + // Caches url. If the same url is requested multiple times then the cache is used + ImageResult get_image_urls_for_chapter(const std::string &url); + private: + std::string last_chapter_url; + std::vector last_chapter_image_urls; + std::mutex image_urls_mutex; + }; +} \ No newline at end of file diff --git a/plugins/Manganelo.hpp b/plugins/Manganelo.hpp index 3405de2..ffac830 100644 --- a/plugins/Manganelo.hpp +++ b/plugins/Manganelo.hpp @@ -1,26 +1,24 @@ #pragma once -#include "Plugin.hpp" +#include "Manga.hpp" #include #include namespace QuickMedia { - // Return false to stop iteration - using PageCallback = std::function; - - class Manganelo : public Plugin { + class Manganelo : public Manga { public: - Manganelo() : Plugin("manganelo") {} + Manganelo() : Manga("manganelo") {} SearchResult search(const std::string &url, BodyItems &result_items) override; SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override; - ImageResult get_image_by_index(const std::string &url, int index, std::string &image_data); - ImageResult get_number_of_images(const std::string &url, int &num_images); + ImageResult get_number_of_images(const std::string &url, int &num_images) override; bool search_suggestions_has_thumbnails() const override { return true; } bool search_results_has_thumbnails() const override { return false; } int get_search_delay() const override { return 150; } Page get_page_after_search() const override { return Page::EPISODE_LIST; } - ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback); + ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) override; + + bool extract_id_from_url(const std::string &url, std::string &manga_id) override; private: // Caches url. If the same url is requested multiple times then the cache is used ImageResult get_image_urls_for_chapter(const std::string &url); diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp index e19fbcd..b430d57 100644 --- a/plugins/Plugin.hpp +++ b/plugins/Plugin.hpp @@ -42,6 +42,7 @@ namespace QuickMedia { virtual ~Plugin() = default; virtual bool is_image_board() { return false; } + virtual bool is_manga() { return false; } virtual PluginResult get_front_page(BodyItems &result_items) { (void)result_items; return PluginResult::OK; diff --git a/src/Body.cpp b/src/Body.cpp index c892c4e..8ccb3cc 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -112,6 +112,7 @@ namespace QuickMedia { loading_thumbnail = true; thumbnail_load_thread = std::thread([this, result, url]() { std::string texture_data; + // TODO: Cache images instead of redownloading them everytime they appear on the screen if(download_to_string(url, texture_data, {}, program->get_current_plugin()->use_tor) == DownloadResult::OK) { if(result->loadFromMemory(texture_data.data(), texture_data.size())) { //result->generateMipmap(); diff --git a/src/Program.c b/src/Program.c index 3246d54..2509f6f 100644 --- a/src/Program.c +++ b/src/Program.c @@ -1,9 +1,11 @@ #include "../include/Program.h" #include #include +#include #include #include #include +#include #include #define READ_END 0 @@ -20,11 +22,22 @@ int exec_program(const char **args, ProgramOutputCallback output_callback, void return -2; } + pid_t parent_pid = getpid(); + pid_t pid = fork(); if(pid == -1) { perror("Failed to fork"); return -3; } else if(pid == 0) { /* child */ + if(prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) { + perror("prctl(PR_SET_PDEATHSIG, SIGTERM) failed"); + exit(127); + } + + /* Test if the parent died before the above call to prctl */ + if(getppid() != parent_pid) + exit(127); + dup2(fd[WRITE_END], STDOUT_FILENO); close(fd[READ_END]); close(fd[WRITE_END]); @@ -105,12 +118,23 @@ int exec_program_async(const char **args, pid_t *result_process_id) { if(args[0] == NULL) return -1; + pid_t parent_pid = getpid(); + pid_t pid = fork(); if(pid == -1) { int err = errno; perror("Failed to fork"); return -err; } else if(pid == 0) { /* child */ + if(prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) { + perror("prctl(PR_SET_PDEATHSIG, SIGTERM) failed"); + exit(127); + } + + /* Test if the parent died before the above call to prctl */ + if(getppid() != parent_pid) + exit(127); + execvp(args[0], args); } else { /* parent */ if(result_process_id) diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index d7c15b1..5a13179 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -1,5 +1,6 @@ #include "../include/QuickMedia.hpp" #include "../plugins/Manganelo.hpp" +#include "../plugins/Mangadex.hpp" #include "../plugins/Youtube.hpp" #include "../plugins/Pornhub.hpp" #include "../plugins/Fourchan.hpp" @@ -8,6 +9,7 @@ #include "../include/VideoPlayer.hpp" #include "../include/StringUtils.hpp" #include "../include/GoogleCaptcha.hpp" +#include "../include/Notification.hpp" #include #include @@ -91,7 +93,7 @@ namespace QuickMedia { static void usage() { fprintf(stderr, "usage: QuickMedia [--tor]\n"); fprintf(stderr, "OPTIONS:\n"); - fprintf(stderr, "plugin The plugin to use. Should be either 4chan, manganelo, pornhub or youtube\n"); + fprintf(stderr, "plugin The plugin to use. Should be either 4chan, manganelo, mangadex, pornhub or youtube\n"); fprintf(stderr, "--tor Use tor. Disabled by default\n"); fprintf(stderr, "EXAMPLES:\n"); fprintf(stderr, "QuickMedia manganelo\n"); @@ -132,6 +134,9 @@ namespace QuickMedia { if(strcmp(argv[i], "manganelo") == 0) { current_plugin = new Manganelo(); plugin_logo_path = "../../../images/manganelo_logo.png"; + } else if(strcmp(argv[i], "mangadex") == 0) { + current_plugin = new Mangadex(); + plugin_logo_path = "../../../images/mangadex_logo.png"; } else if(strcmp(argv[i], "youtube") == 0) { current_plugin = new Youtube(); plugin_logo_path = "../../../images/yt_logo_rgb_dark_small.png"; @@ -261,31 +266,6 @@ namespace QuickMedia { } } - enum class Urgency { - LOW, - NORMAL, - CRITICAL - }; - - const char* urgency_string(Urgency urgency) { - switch(urgency) { - case Urgency::LOW: - return "low"; - case Urgency::NORMAL: - return "normal"; - case Urgency::CRITICAL: - return "critical"; - } - assert(false); - return nullptr; - } - - static void show_notification(const std::string &title, const std::string &description, Urgency urgency = Urgency::NORMAL) { - const char *args[] = { "notify-send", "-u", urgency_string(urgency), "--", title.c_str(), description.c_str(), nullptr }; - exec_program_async(args, nullptr); - printf("Notification: title: %s, description: %s\n", title.c_str(), description.c_str()); - } - static std::string base64_encode(const std::string &data) { return cppcodec::base64_rfc4648::encode(data); } @@ -315,39 +295,6 @@ namespace QuickMedia { return file_overwrite(path, Json::writeString(json_builder, json)) == 0; } - static bool manga_extract_id_from_url(const std::string &url, std::string &manga_id) { - bool manganelo_website = false; - if(url.find("mangakakalot") != std::string::npos || url.find("manganelo") != std::string::npos) - manganelo_website = true; - - if(manganelo_website) { - size_t index = url.find("manga/"); - if(index == std::string::npos) { - std::string err_msg = "Url "; - err_msg += url; - err_msg += " doesn't contain manga id"; - show_notification("Manga", err_msg, Urgency::CRITICAL); - return false; - } - - manga_id = url.substr(index + 6); - if(manga_id.size() <= 2) { - std::string err_msg = "Url "; - err_msg += url; - err_msg += " doesn't contain manga id"; - show_notification("Manga", err_msg, Urgency::CRITICAL); - return false; - } - return true; - } else { - std::string err_msg = "Unexpected url "; - err_msg += url; - err_msg += " is not manganelo or mangakakalot"; - show_notification("Manga", err_msg, Urgency::CRITICAL); - return false; - } - } - enum class SearchSuggestionTab { ALL, HISTORY @@ -373,14 +320,20 @@ namespace QuickMedia { int selected_tab = 0; // TOOD: Make generic, instead of checking for plugin - if(current_plugin->name == "manganelo") { - Path content_storage_dir = get_storage_dir().join("manga"); + if(current_plugin->is_manga()) { + Path content_storage_dir = get_storage_dir().join(current_plugin->name); if(create_directory_recursive(content_storage_dir) != 0) { show_notification("Storage", "Failed to create directory: " + content_storage_dir.data, Urgency::CRITICAL); exit(1); } + Path credentials_storage_dir = get_storage_dir().join("credentials"); + if(create_directory_recursive(credentials_storage_dir) != 0) { + show_notification("Storage", "Failed to create directory: " + credentials_storage_dir.data, Urgency::CRITICAL); + exit(1); + } // TODO: Make asynchronous - for_files_in_dir_sort_last_modified(content_storage_dir, [&history_body](const std::filesystem::path &filepath) { + // TODO: Make this also work for mangadex. Would require storing both id and name of the manga + for_files_in_dir_sort_last_modified(content_storage_dir, [&history_body, this](const std::filesystem::path &filepath) { Path fullpath(filepath.c_str()); Json::Value body; if(!read_file_as_json(fullpath, body)) { @@ -393,7 +346,10 @@ namespace QuickMedia { if(!filename.empty() && manga_name.isString()) { // TODO: Add thumbnail auto body_item = std::make_unique(manga_name.asString()); - body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string()); + if(current_plugin->name == "manganelo") + body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string()); + else if(current_plugin->name == "mangadex") + body_item->url = "https://mangadex.org/title/" + base64_decode(filename.string()); history_body.items.push_back(std::move(body_item)); } return true; @@ -417,16 +373,17 @@ namespace QuickMedia { return false; } - if(next_page == Page::EPISODE_LIST) { + if(next_page == Page::EPISODE_LIST && current_plugin->is_manga()) { + Manga *manga_plugin = static_cast(current_plugin); if(content_url.empty()) { show_notification("Manga", "Url is missing for manga!", Urgency::CRITICAL); return false; } - Path content_storage_dir = get_storage_dir().join("manga"); + Path content_storage_dir = get_storage_dir().join(current_plugin->name); std::string manga_id; - if(!manga_extract_id_from_url(content_url, manga_id)) + if(!manga_plugin->extract_id_from_url(content_url, manga_id)) return false; manga_id_base64 = base64_encode(manga_id); @@ -513,7 +470,9 @@ namespace QuickMedia { if(!update_search_text.empty() && !search_running) { search_suggestion_future = std::async(std::launch::async, [this, update_search_text]() { BodyItems result; - SuggestionResult suggestion_result = current_plugin->update_search_suggestions(update_search_text, result); + if(current_plugin->update_search_suggestions(update_search_text, result) != SuggestionResult::OK) { + show_notification("Search", "Search failed!", Urgency::CRITICAL); + } return result; }); update_search_text.clear(); @@ -1004,7 +963,7 @@ namespace QuickMedia { return LoadImageResult::FAILED; } } else { - show_notification("Manganelo", "Failed to load image for page " + std::to_string(image_index + 1) + ". Image filepath: " + image_path.data, Urgency::CRITICAL); + show_notification("Manga", "Failed to load image for page " + std::to_string(image_index + 1) + ". Image filepath: " + image_path.data, Urgency::CRITICAL); error_message = std::string("Failed to load image for page ") + std::to_string(image_index + 1); return LoadImageResult::FAILED; } @@ -1014,7 +973,7 @@ namespace QuickMedia { } } - void Program::download_chapter_images_if_needed(Manganelo *image_plugin) { + void Program::download_chapter_images_if_needed(Manga *image_plugin) { if(downloading_chapter_url == images_url) return; @@ -1044,6 +1003,7 @@ namespace QuickMedia { Path image_filepath = content_cache_dir_; image_filepath.join(image_filename); #endif + // TODO: Save image with the file extension that url says it has? right now the file is saved without any extension Path image_filepath = content_cache_dir_; image_filepath.join(std::to_string(page++)); @@ -1053,22 +1013,27 @@ namespace QuickMedia { std::string image_content; if(download_to_string(url, image_content, {}, current_plugin->use_tor) != DownloadResult::OK || image_content.size() <= 255) { - bool try_backup_url = false; - std::string new_url = url; - if(string_replace_all(new_url, "s3.mkklcdnv3.com", "bu.mkklcdnbuv1.com") > 0) { - try_backup_url = true; - } else { - try_backup_url = (string_replace_all(new_url, "s41.mkklcdnv41.com", "bu.mkklcdnbuv1.com") > 0); - } + if(current_plugin->name == "manganelo") { + bool try_backup_url = false; + std::string new_url = url; + if(string_replace_all(new_url, "s3.mkklcdnv3.com", "bu.mkklcdnbuv1.com") > 0) { + try_backup_url = true; + } else { + try_backup_url = (string_replace_all(new_url, "s41.mkklcdnv41.com", "bu.mkklcdnbuv1.com") > 0); + } - if(try_backup_url) { - image_content.clear(); - if(download_to_string(new_url, image_content, {}, current_plugin->use_tor) != DownloadResult::OK || image_content.size() <= 255) { - show_notification("Manganelo", "Failed to download image: " + new_url, Urgency::CRITICAL); + if(try_backup_url) { + image_content.clear(); + if(download_to_string(new_url, image_content, {}, current_plugin->use_tor) != DownloadResult::OK || image_content.size() <= 255) { + show_notification("Manganelo", "Failed to download image: " + new_url, Urgency::CRITICAL); + return false; + } + } else { + show_notification("Manganelo", "Failed to download image: " + url, Urgency::CRITICAL); return false; } } else { - show_notification("Manganelo", "Failed to download image: " + url, Urgency::CRITICAL); + show_notification("Manga", "Failed to download image: " + url, Urgency::CRITICAL); return false; } } @@ -1096,12 +1061,12 @@ namespace QuickMedia { sf::Text error_message("", font, 30); error_message.setFillColor(sf::Color::White); - assert(current_plugin->name == "manganelo"); - Manganelo *image_plugin = static_cast(current_plugin); + assert(current_plugin->is_manga()); + Manga *image_plugin = static_cast(current_plugin); std::string image_data; bool download_in_progress = false; - content_cache_dir = get_cache_dir().join("manga").join(manga_id_base64).join(base64_encode(chapter_title)); + content_cache_dir = get_cache_dir().join(image_plugin->name).join(manga_id_base64).join(base64_encode(chapter_title)); if(create_directory_recursive(content_cache_dir) != 0) { show_notification("Storage", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL); current_page = Page::EPISODE_LIST; diff --git a/src/Storage.cpp b/src/Storage.cpp index 919044e..745ecfc 100644 --- a/src/Storage.cpp +++ b/src/Storage.cpp @@ -89,13 +89,17 @@ namespace QuickMedia { } int file_get_content(const Path &path, std::string &result) { - assert(get_file_type(path) == FileType::REGULAR); FILE *file = fopen(path.data.c_str(), "rb"); if(!file) return -errno; fseek(file, 0, SEEK_END); - size_t file_size = ftell(file); + long file_size = ftell(file); + if(file_size == -1) { + fprintf(stderr, "Error: attempted to read directory %s as a file\n", path.data.c_str()); + fclose(file); + return -1; + } fseek(file, 0, SEEK_SET); result.resize(file_size); diff --git a/src/plugins/Mangadex.cpp b/src/plugins/Mangadex.cpp new file mode 100644 index 0000000..43368af --- /dev/null +++ b/src/plugins/Mangadex.cpp @@ -0,0 +1,377 @@ +#include "../../plugins/Mangadex.hpp" +#include "../../include/Storage.hpp" +#include +#include + +static const std::string mangadex_url = "https://mangadex.org"; +// TODO: Allow selecting other languages than english +static const char *language_code = "1"; // english +static const std::string useragent_str = "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36"; + +namespace QuickMedia { + static std::string title_url_extract_manga_id(const std::string &url) { + size_t find_index = url.find("/title/"); + if(find_index == std::string::npos) + return ""; + + size_t id_start_index = find_index + 7; + size_t end_index = url.find("/", id_start_index); + if(end_index == std::string::npos) + return url.substr(id_start_index); + + return url.substr(id_start_index, end_index - id_start_index); + } + + static std::string chapter_url_extract_manga_id(const std::string &url) { + size_t find_index = url.find("/chapter/"); + if(find_index == std::string::npos) + return ""; + return url.substr(find_index + 9); + } + + struct BodyItemChapterContext { + BodyItems *body_items; + int prev_chapter_number; + bool *is_last_page; + }; + + SearchResult Mangadex::search_page(const std::string &url, BodyItems &result_items, int page, bool *is_last_page) { + *is_last_page = true; + CommandArg user_agent_arg = { "-H", useragent_str }; + + std::string chapter_url = url; + if(chapter_url[0] != '/') + chapter_url += "/"; + chapter_url += "chapters/" + std::to_string(page) + "/"; + std::string website_data; + if(download_to_string(chapter_url, website_data, {std::move(user_agent_arg)}, use_tor) != DownloadResult::OK) + return SearchResult::NET_ERR; + + std::string manga_id = title_url_extract_manga_id(chapter_url); + std::string query = "//div[data-manga-id='" + manga_id + "']"; + + BodyItemChapterContext body_item_chapter_context; + body_item_chapter_context.body_items = &result_items; + body_item_chapter_context.prev_chapter_number = -1; + body_item_chapter_context.is_last_page = is_last_page; + + 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, query.c_str(), + [](QuickMediaHtmlNode *node, void *userdata) { + auto *item_data = (BodyItemChapterContext*)userdata; + const char *data_lang = quickmedia_html_node_get_attribute_value(node, "data-lang"); + if(strcmp(data_lang, language_code) != 0) + return; + + const char *chapter_id = quickmedia_html_node_get_attribute_value(node, "data-id"); + if(!chapter_id) + return; + + const char *chapter_number_str = quickmedia_html_node_get_attribute_value(node, "data-chapter"); + if(!chapter_number_str) + return; + + int chapter_number = atoi(chapter_number_str); + if(chapter_number == 0 || chapter_number == item_data->prev_chapter_number) + return; + + item_data->prev_chapter_number = chapter_number; + + const char *chapter_title = quickmedia_html_node_get_attribute_value(node, "data-title"); + std::string chapter_url = mangadex_url + "/chapter/" + chapter_id; + std::string chapter_name = std::string("Ch. ") + chapter_number_str; + if(chapter_title) + chapter_name += std::string(" - ") + chapter_title; + + auto item = std::make_unique(std::move(chapter_name)); + item->url = std::move(chapter_url); + item_data->body_items->push_back(std::move(item)); + *item_data->is_last_page = false; + }, &body_item_chapter_context); + + cleanup: + quickmedia_html_search_deinit(&html_search); + return result == 0 ? SearchResult::OK : SearchResult::ERR; + } + + // TODO: Make pagination asynchronous and make it go to the next page when navigating to the bottom in the list of chapters + // in the GUI. Currently all pages are fetched at once, synchronously. This can be very slow for certain manga like Naruto + // which has 21 pages of chapters... + SearchResult Mangadex::search(const std::string &url, BodyItems &result_items) { +#if 0 + int page = 1; + while(true) { + bool is_last_page; + SearchResult search_result = search_page(url, result_items, page, &is_last_page); + if(search_result != SearchResult::OK) + return search_result; + + ++page; + if(is_last_page) + break; + } + return SearchResult::OK; +#else + CommandArg user_agent_arg = { "-H", useragent_str }; + + std::string manga_id = title_url_extract_manga_id(url); + std::string request_url = "https://mangadex.org/api/?id=" + manga_id + "&type=manga"; + std::string server_response; + if(download_to_string(request_url, server_response, {std::move(user_agent_arg)}, use_tor) != DownloadResult::OK) + return SearchResult::NET_ERR; + + if(server_response.empty()) + return SearchResult::OK; + + 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[0], &server_response[server_response.size()], &json_root, &json_errors)) { + fprintf(stderr, "Mangadex search json error: %s\n", json_errors.c_str()); + return SearchResult::ERR; + } + + Json::Value &status_json = json_root["status"]; + if(!status_json.isString() || status_json.asString() != "OK") + return SearchResult::ERR; + + Json::Value &chapter_json = json_root["chapter"]; + if(!chapter_json.isObject()) + return SearchResult::ERR; + + std::vector> chapters(chapter_json.size()); + for(auto &member_name : chapter_json.getMemberNames()) { + Json::Value chapter = chapter_json[member_name]; + if(chapter.isObject()) + chapters.push_back(std::make_pair(member_name, std::move(chapter))); + } + + std::sort(chapters.begin(), chapters.end(), [](std::pair &a, std::pair &b) { + Json::Value &a_timestamp_json = a.second["timestamp"]; + Json::Value &b_timestamp_json = b.second["timestamp"]; + int64_t a_timestamp = 0; + int64_t b_timestamp = 0; + if(a_timestamp_json.isInt64()) + a_timestamp = a_timestamp_json.asInt64(); + if(b_timestamp_json.isInt64()) + b_timestamp = b_timestamp_json.asInt64(); + return a_timestamp > b_timestamp; + }); + + int prev_chapter_number = -1; + for(auto it = chapters.begin(); it != chapters.end(); ++it) { + const std::string &chapter_id = it->first; + Json::Value &chapter = it->second; + + Json::Value &lang_code = chapter["lang_code"]; + // TODO: Allow selecting other languages than english + if(!lang_code.isString() || lang_code.asString() != "gb") + continue; + + Json::Value &chapter_number_json = chapter["chapter"]; + if(!chapter_number_json.isString()) + continue; + + int chapter_number = atoi(chapter_number_json.asCString()); + if(chapter_number == 0 || chapter_number == prev_chapter_number) + continue; + prev_chapter_number = chapter_number; + + Json::Value &chapter_title_json = chapter["title"]; + std::string chapter_url = mangadex_url + "/chapter/" + chapter_id; + std::string chapter_name = std::string("Ch. ") + chapter_number_json.asCString(); + if(chapter_title_json.isString()) + chapter_name += std::string(" - ") + chapter_title_json.asCString(); + + auto item = std::make_unique(std::move(chapter_name)); + item->url = std::move(chapter_url); + result_items.push_back(std::move(item)); + } + return SearchResult::OK; +#endif + } + + static bool get_rememberme_token(std::string &rememberme_token) { + Path mangadex_credentials_path = get_storage_dir().join("credentials").join("mangadex.json"); + std::string mangadex_credentials; + if(file_get_content(mangadex_credentials_path, mangadex_credentials) != 0) { + fprintf(stderr, "Failed to get content of file: %s\n", mangadex_credentials_path.data.c_str()); + return false; + } + + Json::Value json_root; + Json::CharReaderBuilder json_builder; + std::unique_ptr json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(&mangadex_credentials[0], &mangadex_credentials[mangadex_credentials.size()], &json_root, &json_errors)) { + fprintf(stderr, "Mangadex credentials json error: %s\n", json_errors.c_str()); + return false; + } + + if(json_root.isObject()) { + Json::Value &rememberme_token_json = json_root["rememberme_token"]; + if(rememberme_token_json.isString()) { + rememberme_token = rememberme_token_json.asString(); + return true; + } + } + 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 }; + CommandArg user_agent_arg = { "-H", useragent_str }; + + std::string website_data; + if(download_to_string(url, website_data, {std::move(cookie_arg), std::move(user_agent_arg)}, use_tor) != 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 = std::make_unique(strip(title)); + item->url = mangadex_url + href; + item_data->push_back(std::move(item)); + } + }, &result_items); + + 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) { + if(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); + + cleanup: + quickmedia_html_search_deinit(&html_search); + return result == 0 ? SuggestionResult::OK : SuggestionResult::ERR; + } + + ImageResult Mangadex::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; + } + + ImageResult Mangadex::get_image_urls_for_chapter(const std::string &url) { + if(url == last_chapter_url) + return ImageResult::OK; + + last_chapter_image_urls.clear(); + + CommandArg user_agent_arg = { "-H", useragent_str }; + std::string manga_id = chapter_url_extract_manga_id(url); + std::string request_url = mangadex_url + "/api/?id=" + manga_id + "&server=null&type=chapter"; + std::string server_response; + if(download_to_string(request_url, server_response, {std::move(user_agent_arg)}, use_tor) != DownloadResult::OK) + return ImageResult::NET_ERR; + + if(server_response.empty()) + return ImageResult::OK; + + 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[0], &server_response[server_response.size()], &json_root, &json_errors)) { + fprintf(stderr, "Mangadex image urls json error: %s\n", json_errors.c_str()); + return ImageResult::ERR; + } + + Json::Value &status_json = json_root["status"]; + if(!status_json.isString() || status_json.asString() != "OK") + return ImageResult::ERR; + + Json::Value &chapter_hash = json_root["hash"]; + if(!chapter_hash.isString()) + return ImageResult::ERR; + const char *chapter_hash_str = chapter_hash.asCString(); + + Json::Value &server_json = json_root["server"]; + std::string server; + if(server_json.isString()) + server = server_json.asString(); + else + server = mangadex_url + "/data/"; + + Json::Value &page_array = json_root["page_array"]; + if(page_array.isArray()) { + for(const Json::Value &image_name : page_array) { + if(!image_name.isString()) + continue; + + std::string image_url = server + chapter_hash_str + "/" + image_name.asCString(); + last_chapter_image_urls.push_back(std::move(image_url)); + } + } + + last_chapter_url = url; + if(last_chapter_image_urls.empty()) { + last_chapter_url.clear(); + return ImageResult::ERR; + } + return ImageResult::OK; + } + + ImageResult Mangadex::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; + + image_urls = last_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; + } +} diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp index a97f75a..89f882c 100644 --- a/src/plugins/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -1,4 +1,5 @@ #include "../../plugins/Manganelo.hpp" +#include "../../include/Notification.hpp" #include #include @@ -159,4 +160,37 @@ namespace QuickMedia { } return ImageResult::OK; } + + bool Manganelo::extract_id_from_url(const std::string &url, std::string &manga_id) { + bool manganelo_website = false; + if(url.find("mangakakalot") != std::string::npos || url.find("manganelo") != std::string::npos) + manganelo_website = true; + + if(manganelo_website) { + size_t index = url.find("manga/"); + if(index == std::string::npos) { + std::string err_msg = "Url "; + err_msg += url; + err_msg += " doesn't contain manga id"; + show_notification("Manga", err_msg, Urgency::CRITICAL); + return false; + } + + manga_id = url.substr(index + 6); + if(manga_id.size() <= 2) { + std::string err_msg = "Url "; + err_msg += url; + err_msg += " doesn't contain manga id"; + show_notification("Manga", err_msg, Urgency::CRITICAL); + return false; + } + return true; + } else { + std::string err_msg = "Unexpected url "; + err_msg += url; + err_msg += " is not manganelo or mangakakalot"; + show_notification("Manga", err_msg, Urgency::CRITICAL); + return false; + } + } } \ No newline at end of file -- cgit v1.2.3