#include "../../plugins/Mangadex.hpp" #include "../../include/Utils.hpp" #include "../../include/Theme.hpp" #include namespace QuickMedia { static std::string relationships_get_cover_art_filename(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 &relationship_type_json = relationship_json["type"]; if(!relationship_type_json.isString() || strcmp(relationship_type_json.asCString(), "cover_art") != 0) continue; const Json::Value &attributes_json = relationship_json["attributes"]; if(!attributes_json.isObject()) continue; const Json::Value &file_name_json = attributes_json["fileName"]; if(!file_name_json.isString()) continue; result = file_name_json.asString(); break; } return result; } static void add_tags(const Json::Value &tags_json, BodyItem *body_item) { if(!tags_json.isArray()) return; for(const Json::Value &tag_json : tags_json) { if(!tag_json.isObject()) continue; const Json::Value &attributes_json = tag_json["attributes"]; if(!attributes_json.isObject()) continue; const Json::Value &name_json = attributes_json["name"]; if(!name_json.isObject()) continue; const Json::Value &en_name_json = name_json["en"]; if(!en_name_json.isString()) continue; body_item->add_reaction(en_name_json.asString(), nullptr); } } enum class SearchType { TITLE, AUTHOR }; static PluginResult search_manga(Page *plugin_page, SearchType search_type, const std::string &query, int page, BodyItems &result_items) { std::string url = "https://api.mangadex.org/manga?limit=20&order[relevance]=desc&includes[]=cover_art&offset=" + std::to_string(page * 20); const std::string query_encoded = url_param_encode(query); switch(search_type) { case SearchType::TITLE: url += "&title=" + query_encoded; break; case SearchType::AUTHOR: url += "&authors[]=" + query_encoded; url += "&artists[]=" + query_encoded; break; } Json::Value json_root; if(plugin_page->download_json(json_root, url, {}, true) != DownloadResult::OK) return PluginResult::NET_ERR; if(!json_root.isObject()) return PluginResult::OK; const Json::Value &result_json = json_root["result"]; if(!result_json.isString() || strcmp(result_json.asCString(), "ok") != 0) return PluginResult::ERR; const Json::Value &data_list_json = json_root["data"]; if(!data_list_json.isArray()) return PluginResult::ERR; for(const Json::Value &data_json : data_list_json) { 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"]; const Json::Value &title_ja_json = title_json["ja"]; if(title_en_json.isString()) title = title_en_json.asString(); else if(title_ja_json.isString()) title = title_ja_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(); std::string description; const Json::Value &status_json = attributes_json["status"]; if(status_json.isString()) { if(!description.empty()) description += '\n'; description += "Status: " + status_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()) { if(!description.empty()) description += '\n'; description += en_json.asString(); } } if(!description.empty()) { body_item->set_description(std::move(description)); body_item->set_description_color(get_theme().faded_text_color); } add_tags(attributes_json["tags"], body_item.get()); std::string cover_art_filename = relationships_get_cover_art_filename(data_json["relationships"]); if(!cover_art_filename.empty()) body_item->thumbnail_url = "https://uploads.mangadex.org/covers/" + body_item->url + "/" + std::move(cover_art_filename) + ".256.jpg"; body_item->thumbnail_size = {101, 141}; result_items.push_back(std::move(body_item)); } return PluginResult::OK; } SearchResult MangadexSearchPage::search(const std::string &str, BodyItems &result_items) { return plugin_result_to_search_result(search_manga(this, SearchType::TITLE, str, 0, result_items)); } PluginResult MangadexSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) { return search_manga(this, SearchType::TITLE, str, page, result_items); } static PluginResult get_chapters_for_manga(Page *page, const std::string &manga_id, int page_num, BodyItems &result_items) { 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 &result_json = json_root["result"]; if(!result_json.isString() || strcmp(result_json.asCString(), "ok") != 0) return PluginResult::ERR; const Json::Value &data_list_json = json_root["data"]; if(!data_list_json.isArray()) return PluginResult::ERR; std::string prev_chapter; std::string chapter; for(const Json::Value &data_json : data_list_json) { 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()) chapter = chapter_json.asString(); else chapter = "0"; if(strcmp(prev_chapter.c_str(), chapter.c_str()) == 0) continue; std::string title = "Ch. " + chapter; prev_chapter = std::move(chapter); const Json::Value &title_json = attributes_json["title"]; if(title_json.isString() && title_json.asCString()[0] != '\0') title += " - " + title_json.asString(); 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); } } result_items.push_back(std::move(body_item)); } return PluginResult::OK; } static void get_creators(const Json::Value &relationships_json, std::vector &creators) { if(!relationships_json.isArray()) return; for(const Json::Value &relationship_json : relationships_json) { if(!relationship_json.isObject()) continue; const Json::Value &id_json = relationship_json["id"]; if(!id_json.isString()) continue; const Json::Value &type_json = relationship_json["type"]; if(!type_json.isString() || (strcmp(type_json.asCString(), "author") != 0 && strcmp(type_json.asCString(), "artist") != 0)) continue; const Json::Value &attributes_json = relationship_json["attributes"]; if(!attributes_json.isObject()) continue; const Json::Value &name_json = attributes_json["name"]; if(!name_json.isString()) continue; Creator creator; creator.name = name_json.asString(); creator.url = id_json.asString(); creator.creator_type = strcmp(type_json.asCString(), "author") == 0 ? CreatorType::AUTHOR : CreatorType::ARTIST; creators.push_back(std::move(creator)); } } static PluginResult get_creators_for_manga(Page *page, const std::string &manga_id, std::vector &creators) { std::string request_url = "https://api.mangadex.org/manga/" + manga_id + "?includes[]=artist&includes[]=author"; 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 &result_json = json_root["result"]; if(!result_json.isString() || strcmp(result_json.asCString(), "ok") != 0) return PluginResult::ERR; const Json::Value &data_json = json_root["data"]; if(!data_json.isObject()) return PluginResult::ERR; get_creators(data_json["relationships"], creators); return PluginResult::OK; } static void remove_duplicate_creators(std::vector &creators) { std::sort(creators.begin(), creators.end(), [](const Creator &creator1, const Creator &creator2) { return creator1.name < creator2.name; }); creators.erase( std::unique(creators.begin(), creators.end(), [](const Creator &creator1, const Creator &creator2) { return creator1.name == creator2.name; }), creators.end()); } // TODO: Do requests in parallel PluginResult MangadexSearchPage::submit(const SubmitArgs &args, std::vector &result_tabs) { PluginResult result; BodyItems body_items; result = get_chapters_for_manga(this, args.url, 0, body_items); if(result != PluginResult::OK) return result; auto body = create_body(); body->set_items(std::move(body_items)); auto chapters_page = std::make_unique(program, this, args.title, args.url, args.thumbnail_url); result_tabs.push_back(Tab{std::move(body), std::move(chapters_page), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); std::vector creators; result = get_creators_for_manga(this, args.url, creators); if(result != PluginResult::OK) return result; remove_duplicate_creators(creators); for(Creator &creator : creators) { result_tabs.push_back(Tab{create_body(), std::make_unique(program, this, std::move(creator)), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); } return PluginResult::OK; } PluginResult MangadexChaptersPage::submit(const SubmitArgs &args, std::vector &result_tabs) { result_tabs.push_back(Tab{nullptr, std::make_unique(program, search_page, content_title, args.url, args.title, thumbnail_url), 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); } PluginResult MangadexCreatorPage::submit(const SubmitArgs &args, std::vector &result_tabs) { return search_page->submit(args, result_tabs); } PluginResult MangadexCreatorPage::get_page(const std::string&, int page, BodyItems &result_items) { return search_manga(this, SearchType::AUTHOR, creator.url, page, result_items); } PluginResult MangadexCreatorPage::lazy_fetch(BodyItems &result_items) { return search_manga(this, SearchType::AUTHOR, creator.url, 0, result_items); } 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::update_image_urls(int &num_images) { num_images = 0; Json::Value json_root; // TODO: If this fails, call this again to retrieve another server. Also do the same for automedia 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 &result_json = json_root["result"]; if(!result_json.isString() || strcmp(result_json.asCString(), "ok") != 0) return ImageResult::ERR; const Json::Value &base_url_json = json_root["baseUrl"]; if(!base_url_json.isString()) return ImageResult::ERR; const Json::Value &chapter_json = json_root["chapter"]; if(!chapter_json.isObject()) return ImageResult::ERR; const Json::Value &hash_json = chapter_json["hash"]; if(!hash_json.isString()) return ImageResult::ERR; const Json::Value &data_json = chapter_json["data"]; if(!data_json.isArray()) return ImageResult::ERR; std::string base_url = base_url_json.asString(); if(!base_url.empty() && base_url.back() != '/') base_url += '/'; const std::string hash_str = hash_json.asString(); for(const Json::Value &data_item_json : data_json) { if(!data_item_json.isString()) continue; chapter_image_urls.push_back(base_url + "data/" + hash_str + "/" + data_item_json.asString()); } num_images = chapter_image_urls.size(); return ImageResult::OK; } ImageResult MangadexImagesPage::for_each_page_in_chapter(PageCallback callback) { for(const std::string &url : chapter_image_urls) { if(!callback(url)) break; } return ImageResult::OK; } }