#include "../../plugins/Mangadex.hpp" #include "../../include/Storage.hpp" #include "../../include/Notification.hpp" #include "../../include/StringUtils.hpp" #include "../../include/NetUtils.hpp" #include #include #include #include static const std::string mangadex_url = "https://mangadex.org"; static rapidjson::Value nullValue(rapidjson::kNullType); static const rapidjson::Value& GetMember(const rapidjson::Value &obj, const char *key) { auto it = obj.FindMember(key); if(it != obj.MemberEnd()) return it->value; return nullValue; } 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); } 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("QuickMedia", "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 MangadexSearchPage::search(const std::string &str, int page, BodyItems &result_items) { std::string rememberme_token; if(!get_rememberme_token(rememberme_token)) return SearchResult::ERR; std::string url = "https://mangadex.org/search?s=0&p=" + std::to_string(page) + "&tag_mode_inc=all&tag_mode_exc=any&title=" + url_param_encode(str) +"#listing"; 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)}, 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; } SearchResult MangadexSearchPage::search(const std::string &str, BodyItems &result_items) { return search(str, 1, result_items); } PluginResult MangadexSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { return search_result_to_plugin_result(search(str, 1 + page, result_items)); } PluginResult MangadexSearchPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { std::string request_url = "https://mangadex.org/api/?id=" + title_url_extract_manga_id(url) + "&type=manga"; rapidjson::Document json_root; DownloadResult result = download_to_json(request_url, json_root, {}, true); if(result != DownloadResult::OK) return download_result_to_plugin_result(result); if(!json_root.IsObject()) return PluginResult::ERR; const rapidjson::Value &status_json = GetMember(json_root, "status"); if(!status_json.IsString() || strcmp(status_json.GetString(), "OK") != 0) return PluginResult::ERR; const rapidjson::Value &chapter_json = GetMember(json_root, "chapter"); if(!chapter_json.IsObject()) return PluginResult::ERR; time_t time_now = time(NULL); auto body = create_body(); /* TODO: Pointer */ std::string prev_chapter_number; for(auto const &it : chapter_json.GetObject()) { const std::string &chapter_id = it.name.GetString(); const rapidjson::Value &chapter = it.value; const rapidjson::Value ×tamp_json = chapter["timestamp"]; if(timestamp_json.IsInt64() && timestamp_json.GetInt64() > time_now) continue; const rapidjson::Value &lang_code = chapter["lang_code"]; // TODO: Allow selecting other languages than english if(!lang_code.IsString() || strcmp(lang_code.GetString(), "gb") != 0) continue; const rapidjson::Value &chapter_number_json = chapter["chapter"]; if(!chapter_number_json.IsString()) continue; std::string chapter_number_str = chapter_number_json.GetString(); if(chapter_number_str == prev_chapter_number) continue; prev_chapter_number = chapter_number_str; const rapidjson::Value &chapter_title_json = chapter["title"]; std::string chapter_url = mangadex_url + "/chapter/" + chapter_id; std::string chapter_name = std::string("Ch. ") + chapter_number_str; if(chapter_title_json.IsString() && chapter_title_json.GetStringLength() > 0) chapter_name += std::string(" - ") + chapter_title_json.GetString(); auto item = BodyItem::create(std::move(chapter_name)); item->url = std::move(chapter_url); body->items.push_back(std::move(item)); } result_tabs.push_back(Tab{std::move(body), std::make_unique(program, title, url), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); return PluginResult::OK; } bool MangadexSearchPage::get_rememberme_token(std::string &rememberme_token_output) { if(rememberme_token) { rememberme_token_output = rememberme_token.value(); return true; } 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_output = rememberme_token_json.asString(); } rememberme_token = rememberme_token_output; return true; } PluginResult MangadexChaptersPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { result_tabs.push_back(Tab{nullptr, std::make_unique(program, content_title, title, url), nullptr}); return PluginResult::OK; } bool MangadexChaptersPage::extract_id_from_url(const std::string &url, std::string &manga_id) const { manga_id = title_url_extract_manga_id(url); return true; } 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 = chapter_image_urls.size(); return ImageResult::OK; } 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)}, true) != DownloadResult::OK) return false; return true; } ImageResult MangadexImagesPage::get_image_urls_for_chapter(const std::string &url) { if(!chapter_image_urls.empty()) return ImageResult::OK; std::string cookie_filepath; if(!get_cookie_filepath(cookie_filepath)) return ImageResult::ERR; if(!save_mangadex_cookies(url, cookie_filepath)) return ImageResult::ERR; CommandArg cookie_arg = { "-b", std::move(cookie_filepath) }; std::string manga_id = chapter_url_extract_manga_id(url); std::string request_url = mangadex_url + "/api/?id=" + manga_id + "&server=null&type=chapter"; Json::Value json_root; DownloadResult result = download_json(json_root, request_url, {std::move(cookie_arg)}, true); if(result != DownloadResult::OK) return download_result_to_image_result(result); if(!json_root.isObject()) 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(); chapter_image_urls.push_back(std::move(image_url)); } } if(chapter_image_urls.empty()) return ImageResult::ERR; return ImageResult::OK; } ImageResult MangadexImagesPage::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; } }