#include "../../plugins/Mangadex.hpp" #include "../../include/Utils.hpp" #include "../../include/Theme.hpp" #include namespace QuickMedia { PluginResult legacy_mangadex_id_to_new_manga_id(Page *page, const std::vector &manga_ids, std::vector> &new_manga_ids) { Json::Value request_json(Json::objectValue); request_json["type"] = "manga"; Json::Value manga_ids_json(Json::arrayValue); for(int manga_id : manga_ids) { manga_ids_json.append(manga_id); } request_json["ids"] = std::move(manga_ids_json); Json::StreamWriterBuilder json_builder; json_builder["commentStyle"] = "None"; json_builder["indentation"] = ""; std::vector additional_args = { { "-X", "POST" }, { "-H", "content-type: application/json" }, { "--data-binary", Json::writeString(json_builder, request_json) } }; Json::Value json_root; if(page->download_json(json_root, "https://api.mangadex.org/legacy/mapping", std::move(additional_args), true) != DownloadResult::OK) return PluginResult::NET_ERR; if(!json_root.isArray()) return PluginResult::OK; for(const Json::Value &result_item_json : json_root) { if(!result_item_json.isObject()) continue; const Json::Value &result_json = result_item_json["result"]; if(!result_json.isString() || strcmp(result_json.asCString(), "ok") != 0) continue; const Json::Value &data_json = result_item_json["data"]; if(!data_json.isObject()) continue; const Json::Value &attributes_json = data_json["attributes"]; if(!attributes_json.isObject()) continue; const Json::Value &legacy_id_json = attributes_json["legacyId"]; if(!legacy_id_json.isInt()) continue; const Json::Value &new_id_json = attributes_json["newId"]; if(!new_id_json.isString()) continue; new_manga_ids.push_back(std::make_pair(legacy_id_json.asInt(), new_id_json.asString())); } return PluginResult::OK; } static std::shared_ptr relationship_get_body_item(const Json::Value &json, BodyItems &body_items) { for(const Json::Value &item_json : json) { if(!item_json.isObject()) continue; const Json::Value &type_json = item_json["type"]; if(!type_json.isString() || strcmp(type_json.asCString(), "manga") != 0) continue; const Json::Value &id_json = item_json["id"]; if(!id_json.isString()) continue; std::string id_str = id_json.asString(); auto it = std::find_if(body_items.begin(), body_items.end(), [&id_str](const std::shared_ptr &body_item) { return body_item->url == id_str; }); if(it == body_items.end()) continue; return *it; } return nullptr; } PluginResult MangadexSearchPage::get_cover_urls(BodyItems &body_items) { if(body_items.empty()) return PluginResult::OK; std::string url = "https://api.mangadex.org/cover?limit=100&order[updatedAt]=desc"; for(size_t i = 0; i < body_items.size(); ++i) { if(!body_items[i]->thumbnail_url.empty()) url += "&ids[]=" + body_items[i]->thumbnail_url; } Json::Value json_root; if(download_json(json_root, url, {}, true) != DownloadResult::OK) return PluginResult::NET_ERR; if(!json_root.isObject()) return PluginResult::OK; const Json::Value &results_json = json_root["results"]; if(!results_json.isArray()) return PluginResult::OK; std::shared_ptr body_item; for(const Json::Value &result_item_json : results_json) { if(!result_item_json.isObject()) continue; const Json::Value &result_json = result_item_json["result"]; if(!result_json.isString() || strcmp(result_json.asCString(), "ok") != 0) continue; const Json::Value &relationships_json = result_item_json["relationships"]; if(!relationships_json.isArray()) continue; body_item = relationship_get_body_item(relationships_json, body_items); if(!body_item) continue; const Json::Value &data_json = result_item_json["data"]; if(!data_json.isObject()) continue; const Json::Value &attributes_json = data_json["attributes"]; if(!attributes_json.isObject()) continue; const Json::Value &filename_json = attributes_json["fileName"]; if(!filename_json.isString()) continue; body_item->thumbnail_url = "https://uploads.mangadex.org/covers/" + body_item->url + "/" + filename_json.asString() + ".256.jpg"; body_item->thumbnail_size = {101, 141}; } return PluginResult::OK; } static std::string relationships_get_cover_art(const Json::Value &relationships_json) { std::string result; if(!relationships_json.isArray()) return result; for(const Json::Value &relationship_json : relationships_json) { if(!relationship_json.isObject()) continue; const Json::Value &id_json = relationship_json["id"]; const Json::Value &relationship_type_json = relationship_json["type"]; if(!id_json.isString() || !relationship_type_json.isString() || strcmp(relationship_type_json.asCString(), "cover_art") != 0) continue; result = id_json.asString(); break; } return result; } SearchResult MangadexSearchPage::search(const std::string &str, int page, BodyItems &result_items) { std::string url = "https://api.mangadex.org/manga?title=" + url_param_encode(str) + "&limit=20&offset=" + std::to_string(page * 20); Json::Value json_root; if(download_json(json_root, url, {}, true) != DownloadResult::OK) return SearchResult::NET_ERR; if(!json_root.isObject()) return SearchResult::OK; const Json::Value &results_json = json_root["results"]; if(!results_json.isArray()) return SearchResult::OK; for(const Json::Value &result_item_json : results_json) { if(!result_item_json.isObject()) continue; const Json::Value &result_json = result_item_json["result"]; if(!result_json.isString() || strcmp(result_json.asCString(), "ok") != 0) continue; const Json::Value &data_json = result_item_json["data"]; if(!data_json.isObject()) continue; const Json::Value &id_json = data_json["id"]; if(!id_json.isString()) continue; const Json::Value &attributes_json = data_json["attributes"]; if(!attributes_json.isObject()) continue; const Json::Value &title_json = attributes_json["title"]; if(!title_json.isObject()) continue; std::string title; const Json::Value &title_en_json = title_json["en"]; if(title_en_json.isString()) title = title_en_json.asString(); else title = "No title"; // TODO: Verify if this happens. If it happens, get the title in any available language auto body_item = BodyItem::create(std::move(title)); body_item->url = id_json.asString(); const Json::Value &description_json = attributes_json["description"]; if(description_json.isObject()) { const Json::Value &en_json = description_json["en"]; if(en_json.isString()) { body_item->set_description(en_json.asString()); body_item->set_description_color(get_theme().faded_text_color); } } body_item->thumbnail_url = relationships_get_cover_art(result_item_json["relationships"]); result_items.push_back(std::move(body_item)); } // Intentionally ignore errors. This api shouldn't fail if we fail to get covers get_cover_urls(result_items); return SearchResult::OK; } SearchResult MangadexSearchPage::search(const std::string &str, BodyItems &result_items) { return search(str, 0, result_items); } PluginResult MangadexSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { return search_result_to_plugin_result(search(str, page, result_items)); } static PluginResult get_chapters_for_manga(Page *page, const std::string &manga_id, int page_num, BodyItems &result_items, ChapterImageUrls &chapter_image_urls) { std::string request_url = "https://api.mangadex.org/manga/" + manga_id + "/feed?order[chapter]=desc&limit=100&translatedLanguage[]=en&offset=" + std::to_string(page_num * 100); Json::Value json_root; if(page->download_json(json_root, request_url, {}, true) != DownloadResult::OK) return PluginResult::NET_ERR; if(!json_root.isObject()) return PluginResult::OK; const Json::Value &results_json = json_root["results"]; if(!results_json.isArray()) return PluginResult::OK; const char *prev_chapter = nullptr; for(const Json::Value &result_item_json : results_json) { if(!result_item_json.isObject()) continue; const Json::Value &result_json = result_item_json["result"]; if(!result_json.isString() || strcmp(result_json.asCString(), "ok") != 0) continue; const Json::Value &data_json = result_item_json["data"]; if(!data_json.isObject()) continue; const Json::Value &id_json = data_json["id"]; if(!id_json.isString()) continue; const Json::Value &attributes_json = data_json["attributes"]; if(!attributes_json.isObject()) continue; const Json::Value &translated_language_json = attributes_json["translatedLanguage"]; if(!translated_language_json.isString() || strcmp(translated_language_json.asCString(), "en") != 0) continue; const Json::Value &chapter_json = attributes_json["chapter"]; if(!chapter_json.isString()) continue; if(prev_chapter && strcmp(prev_chapter, chapter_json.asCString()) == 0) continue; const Json::Value &hash_json = attributes_json["hash"]; if(!hash_json.isString()) continue; prev_chapter = chapter_json.asCString(); std::string title = "Ch. " + chapter_json.asString(); const Json::Value &title_json = attributes_json["title"]; if(title_json.isString() && title_json.asCString()[0] != '\0') title += " - " + title_json.asString(); const Json::Value &attributes_data_json = attributes_json["data"]; if(!attributes_data_json.isArray()) continue; auto body_item = BodyItem::create(std::move(title)); body_item->url = id_json.asString(); const Json::Value &publish_at_json = attributes_json["publishAt"]; if(publish_at_json.isString()) { time_t unix_time = iso_utc_to_unix_time(publish_at_json.asCString()); if(unix_time != 0) { body_item->set_description("Uploaded: " + unix_time_to_local_time_str(unix_time)); body_item->set_description_color(get_theme().faded_text_color); } } std::vector image_urls; image_urls.reserve(attributes_data_json.size()); for(const Json::Value &data_item_json : attributes_data_json) { if(!data_item_json.isString()) continue; std::string url = hash_json.asString() + "/" + data_item_json.asString(); image_urls.push_back(std::move(url)); } chapter_image_urls[body_item->url] = std::move(image_urls); result_items.push_back(std::move(body_item)); } return PluginResult::OK; } PluginResult MangadexSearchPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { chapter_image_urls.clear(); auto body = create_body(); BodyItems body_items; get_chapters_for_manga(this, url, 0, body_items, chapter_image_urls); body->set_items(std::move(body_items)); result_tabs.push_back(Tab{std::move(body), std::make_unique(program, this, title, url), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); return PluginResult::OK; } 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, search_page, content_title, url, title), nullptr}); return PluginResult::OK; } PluginResult MangadexChaptersPage::get_page(const std::string&, int page, BodyItems &result_items) { return get_chapters_for_manga(this, content_url, page, result_items, search_page->chapter_image_urls); } bool MangadexChaptersPage::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) { manga_id = url; return true; } start_index += 6; 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 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; } ImageResult MangadexImagesPage::for_each_page_in_chapter(PageCallback callback) { ImageResult image_result = get_image_urls_for_chapter(url); if(image_result != ImageResult::OK) return image_result; for(const std::string &url : chapter_image_urls) { if(!callback(url)) break; } return ImageResult::OK; } ImageResult MangadexImagesPage::get_image_urls_for_chapter(const std::string &url) { if(!chapter_image_urls.empty()) return ImageResult::OK; Json::Value json_root; if(download_json(json_root, "https://api.mangadex.org/at-home/server/" + url, {}, true) != DownloadResult::OK) return ImageResult::NET_ERR; if(!json_root.isObject()) return ImageResult::ERR; const Json::Value &base_url_json = json_root["baseUrl"]; if(!base_url_json.isString()) return ImageResult::ERR; std::string base_url = base_url_json.asString(); if(!base_url.empty() && base_url.back() != '/') base_url += '/'; auto it = search_page->chapter_image_urls.find(url); if(it == search_page->chapter_image_urls.end()) return ImageResult::ERR; chapter_image_urls.resize(it->second.size()); for(size_t i = 0; i < it->second.size(); ++i) { chapter_image_urls[i] = base_url + "data/" + it->second[i]; } return ImageResult::OK; } }