From 5cc735b22570f1667d62958e59ce4910b529f5af Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 16 Aug 2021 21:13:24 +0200 Subject: Add MyAnimeList (wip) --- README.md | 4 +- TODO | 4 +- depends/html-search | 2 +- images/mal_logo.png | Bin 0 -> 5858 bytes include/StringUtils.hpp | 1 + plugins/HotExamples.hpp | 4 +- plugins/Info.hpp | 2 +- plugins/MangaCombined.hpp | 1 - plugins/MangaGeneric.hpp | 2 - plugins/Mangadex.hpp | 1 - plugins/Manganelo.hpp | 2 - plugins/Matrix.hpp | 8 +- plugins/MediaGeneric.hpp | 1 - plugins/MyAnimeList.hpp | 36 +++++ plugins/NyaaSi.hpp | 4 +- plugins/Page.hpp | 4 +- src/Body.cpp | 6 +- src/NetUtils.cpp | 16 +-- src/QuickMedia.cpp | 10 +- src/StringUtils.cpp | 20 +++ src/plugins/MangaGeneric.cpp | 2 + src/plugins/Mangadex.cpp | 1 + src/plugins/Manganelo.cpp | 2 + src/plugins/Matrix.cpp | 13 +- src/plugins/MediaGeneric.cpp | 10 +- src/plugins/MyAnimeList.cpp | 311 +++++++++++++++++++++++++++++++++++++++++++ 26 files changed, 415 insertions(+), 52 deletions(-) create mode 100644 images/mal_logo.png create mode 100644 plugins/MyAnimeList.hpp create mode 100644 src/plugins/MyAnimeList.cpp diff --git a/README.md b/README.md index 04bb32f..f7147d5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,13 @@ # QuickMedia A dmenu-inspired native client for web services. -Currently supported web services: `youtube`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao`, `hotexamples` and _others_.\ +Currently supported web services: `youtube`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao`, `hotexamples`, `mal` and _others_.\ Config data, including manga progress is stored under `$HOME/.config/quickmedia`.\ Cache is stored under `$HOME/.cache/quickmedia`. ## Usage ``` usage: quickmedia [plugin] [--use-system-mpv-config] [--dir ] [-e ] [youtube-url] OPTIONS: - plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, file-manager or stdin + plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, mal, file-manager or stdin --no-video Only play audio when playing a video. Disabled by default --use-system-mpv-config Use system mpv config instead of no config. Disabled by default --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default diff --git a/TODO b/TODO index 3431697..2e5fc8e 100644 --- a/TODO +++ b/TODO @@ -197,4 +197,6 @@ Use event timestamp to sort display name/room name (etc) events to apply them in Restart and resume youtube media download in downloader when media is throttled. Try to reconnect media proxy on disconnect. The internet may be unstable (for example on mobile internet). Youtube is still getting throttled, but to 500kb/sec from 5mb/sec. How to detect this???. -Opening a media url should display it directly in quickmedia. \ No newline at end of file +Opening a media url should display it directly in quickmedia. +Automatically resize body item thumbnail if body is small, or move thumbnail above the text. +Support  ? \ No newline at end of file diff --git a/depends/html-search b/depends/html-search index 9cafc14..95c189f 160000 --- a/depends/html-search +++ b/depends/html-search @@ -1 +1 @@ -Subproject commit 9cafc14e2508224c901f5a03bb6fda73f0c51e57 +Subproject commit 95c189f7445e6deca85130b7b8fa25dc76fabe12 diff --git a/images/mal_logo.png b/images/mal_logo.png new file mode 100644 index 0000000..232d983 Binary files /dev/null and b/images/mal_logo.png differ diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp index 888adbb..cc2acf4 100644 --- a/include/StringUtils.hpp +++ b/include/StringUtils.hpp @@ -21,4 +21,5 @@ namespace QuickMedia { size_t str_find_case_insensitive(const std::string &str, size_t start_index, const char *substr, size_t substr_len); char to_upper(char c); bool strcase_equals(const char *str1, const char *str2); + bool to_num(const char *str, size_t size, int &num); } \ No newline at end of file diff --git a/plugins/HotExamples.hpp b/plugins/HotExamples.hpp index 154f1ab..73bc150 100644 --- a/plugins/HotExamples.hpp +++ b/plugins/HotExamples.hpp @@ -10,7 +10,7 @@ namespace QuickMedia { HotExamplesLanguageSelectPage(Program *program) : Page(program) {} const char* get_title() const override { return "Select language"; } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - bool submit_is_async() override { return false; } + bool submit_is_async() const override { return false; } }; class HotExamplesSearchPage : public Page { @@ -29,7 +29,7 @@ namespace QuickMedia { HotExamplesCodeExamplesPage(Program *program, std::string title) : Page(program), title(std::move(title)) {} const char* get_title() const override { return title.c_str(); } PluginResult submit(const std::string&, const std::string&, std::vector&) override { return PluginResult::OK; } - bool submit_is_async() override { return false; } + bool submit_is_async() const override { return false; } private: std::string title; }; diff --git a/plugins/Info.hpp b/plugins/Info.hpp index 4ab39f5..af62282 100644 --- a/plugins/Info.hpp +++ b/plugins/Info.hpp @@ -8,7 +8,7 @@ namespace QuickMedia { InfoPage(Program *program) : Page(program) {} const char* get_title() const override { return "Info"; } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - bool submit_is_async() override { return false; } + bool submit_is_async() const override { return false; } static std::shared_ptr add_url(const std::string &url); static std::shared_ptr add_reverse_image_search(const std::string &image_url); diff --git a/plugins/MangaCombined.hpp b/plugins/MangaCombined.hpp index dafc884..39a143c 100644 --- a/plugins/MangaCombined.hpp +++ b/plugins/MangaCombined.hpp @@ -22,7 +22,6 @@ namespace QuickMedia { SearchResult search(const std::string &str, BodyItems &result_items) override; PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); }; void cancel_operation() override; private: std::vector search_pages; diff --git a/plugins/MangaGeneric.hpp b/plugins/MangaGeneric.hpp index dcd3544..2f65762 100644 --- a/plugins/MangaGeneric.hpp +++ b/plugins/MangaGeneric.hpp @@ -113,7 +113,6 @@ namespace QuickMedia { PluginResult get_page(const std::string &url, bool is_post, const std::vector &form_data, BodyItems &result_items); PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); }; MangaGenericSearchPage& search_handler(const char *search_template, int page_start); MangaGenericSearchPage& search_post_handler(const char *url, std::vector form_data, SearchQueryJsonHandler result_handler); @@ -175,7 +174,6 @@ namespace QuickMedia { const char* get_title() const override { return creator.name.c_str(); } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; PluginResult lazy_fetch(BodyItems &result_items) override; - sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); }; private: MangaGenericSearchPage *search_page; Creator creator; diff --git a/plugins/Mangadex.hpp b/plugins/Mangadex.hpp index 7497b06..cd81c44 100644 --- a/plugins/Mangadex.hpp +++ b/plugins/Mangadex.hpp @@ -16,7 +16,6 @@ namespace QuickMedia { SearchResult search(const std::string &str, BodyItems &result_items) override; PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); }; ChapterImageUrls chapter_image_urls; private: diff --git a/plugins/Manganelo.hpp b/plugins/Manganelo.hpp index 7cf3420..62fd9bf 100644 --- a/plugins/Manganelo.hpp +++ b/plugins/Manganelo.hpp @@ -10,7 +10,6 @@ namespace QuickMedia { bool search_is_filter() override { return false; } SearchResult search(const std::string &str, BodyItems &result_items) override; PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); }; }; class ManganeloChaptersPage : public MangaChaptersPage { @@ -28,7 +27,6 @@ namespace QuickMedia { const char* get_title() const override { return creator.name.c_str(); } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; PluginResult lazy_fetch(BodyItems &result_items) override; - sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); }; private: Creator creator; }; diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp index f589db5..5292414 100644 --- a/plugins/Matrix.hpp +++ b/plugins/Matrix.hpp @@ -319,7 +319,7 @@ namespace QuickMedia { const char* get_title() const override { return title.c_str(); } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - bool submit_is_async() override { return false; } + bool submit_is_async() const override { return false; } bool clear_search_after_submit() override { return true; } void add_body_item(std::shared_ptr body_item); @@ -349,7 +349,7 @@ namespace QuickMedia { MatrixRoomTagsPage(Program *program, Body *body) : Page(program), body(body) {} const char* get_title() const override { return "Tags"; } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - bool submit_is_async() override { return false; } + bool submit_is_async() const override { return false; } bool clear_search_after_submit() override { return true; } void add_room_body_item_to_tag(std::shared_ptr body_item, const std::string &tag); @@ -380,7 +380,7 @@ namespace QuickMedia { const char* get_title() const override { return title.c_str(); } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - bool submit_is_async() override { return false; } + bool submit_is_async() const override { return false; } bool clear_search_after_submit() override { return true; } void add_body_item(std::shared_ptr body_item); @@ -484,7 +484,7 @@ namespace QuickMedia { PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; PluginResult lazy_fetch(BodyItems &result_items) override; bool is_ready() override; - bool submit_is_async() override { return false; } + bool submit_is_async() const override { return false; } bool clear_search_after_submit() override { return true; } void add_notification(MatrixNotification notification); diff --git a/plugins/MediaGeneric.hpp b/plugins/MediaGeneric.hpp index 8885db2..fce0e97 100644 --- a/plugins/MediaGeneric.hpp +++ b/plugins/MediaGeneric.hpp @@ -40,7 +40,6 @@ namespace QuickMedia { SearchResult search(const std::string &str, BodyItems &result_items) override; PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override; PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - sf::Vector2i get_thumbnail_max_size() override { return thumbnail_max_size; }; PluginResult get_related_media(const std::string &url, BodyItems &result_items); diff --git a/plugins/MyAnimeList.hpp b/plugins/MyAnimeList.hpp new file mode 100644 index 0000000..c88e94f --- /dev/null +++ b/plugins/MyAnimeList.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include "Page.hpp" + +namespace QuickMedia { + class MyAnimeListSearchPage : public Page { + public: + MyAnimeListSearchPage(Program *program) : Page(program) {} + const char* get_title() const override { return "Search for anime or manga"; } + bool search_is_filter() override { return false; } + SearchResult search(const std::string &str, BodyItems &result_items) override; + PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; + }; + + class MyAnimeListDetailsPage : public LazyFetchPage { + public: + MyAnimeListDetailsPage(Program *program, std::string url) : LazyFetchPage(program), url(std::move(url)) {} + const char* get_title() const override { return "Details"; } + bool submit_is_async() const override { return false; } + PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; + PluginResult lazy_fetch(BodyItems &result_items) override; + private: + std::string url; + }; + + class MyAnimeListRecommendationsPage : public LazyFetchPage { + public: + MyAnimeListRecommendationsPage(Program *program, std::string url) : LazyFetchPage(program), url(std::move(url)) {} + const char* get_title() const override { return "Recommendations"; } + bool submit_is_async() const override { return false; } + PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; + PluginResult lazy_fetch(BodyItems &result_items) override; + private: + std::string url; + }; +} \ No newline at end of file diff --git a/plugins/NyaaSi.hpp b/plugins/NyaaSi.hpp index 6f161ac..f113544 100644 --- a/plugins/NyaaSi.hpp +++ b/plugins/NyaaSi.hpp @@ -54,7 +54,7 @@ namespace QuickMedia { NyaaSiSortOrderPage(Program *program, Body *body, NyaaSiSearchPage *search_page) : Page(program), body(body), search_page(search_page) {} const char* get_title() const override { return "Sort order"; } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - bool submit_is_async() override { return false; } + bool submit_is_async() const override { return false; } private: Body *body; NyaaSiSearchPage *search_page; @@ -65,6 +65,6 @@ namespace QuickMedia { NyaaSiTorrentPage(Program *program) : Page(program) {} const char* get_title() const override { return "Torrent"; } PluginResult submit(const std::string &title, const std::string &url, std::vector &result_tabs) override; - bool submit_is_async() override { return false; } + bool submit_is_async() const override { return false; } }; } \ No newline at end of file diff --git a/plugins/Page.hpp b/plugins/Page.hpp index 9d5bade..8d033e1 100644 --- a/plugins/Page.hpp +++ b/plugins/Page.hpp @@ -41,7 +41,7 @@ namespace QuickMedia { return PluginResult::ERR; } // Override and return false to make submit run in the main (ui) thread - virtual bool submit_is_async() { return true; } + virtual bool submit_is_async() const { return true; } virtual bool clear_search_after_submit() { return false; } virtual PluginResult submit_suggestion(const std::string &title, const std::string &url, BodyItems &result_items) { (void)title; @@ -73,8 +73,6 @@ namespace QuickMedia { std::unique_ptr create_search_bar(const std::string &placeholder_text, int search_delay); bool load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id); - - virtual sf::Vector2i get_thumbnail_max_size() { return sf::Vector2i(250, 141); }; Program *program; std::shared_ptr submit_body_item; // TODO: Remove this diff --git a/src/Body.cpp b/src/Body.cpp index c3cbcda..b7424c5 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -115,8 +115,8 @@ namespace QuickMedia { embedded_item_load_text = sf::Text("", *FontLoader::get_font(FontLoader::FontType::LATIN), body_spacing[body_theme].embedded_item_font_size); progress_text.setFillColor(get_current_theme().text_color); replies_text.setFillColor(get_current_theme().replies_text_color); - thumbnail_max_size.x = 250; - thumbnail_max_size.y = 141; + thumbnail_max_size.x = 600; + thumbnail_max_size.y = 337; sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y); loading_icon.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f); render_selected_item_bg = !is_touch_enabled(); @@ -923,7 +923,7 @@ namespace QuickMedia { if(item->thumbnail_size.x > 0 && item->thumbnail_size.y > 0) content_size = clamp_to_size(sf::Vector2i(std::floor(item->thumbnail_size.x * get_ui_scale()), std::floor(item->thumbnail_size.y * get_ui_scale())), thumbnail_max_size_scaled); else - content_size = thumbnail_max_size_scaled; + content_size = sf::Vector2i(250 * get_ui_scale(), 141 * get_ui_scale()); return content_size; } diff --git a/src/NetUtils.cpp b/src/NetUtils.cpp index 28256cb..0f957d5 100644 --- a/src/NetUtils.cpp +++ b/src/NetUtils.cpp @@ -34,17 +34,6 @@ namespace QuickMedia { std::string unescaped_str; }; - static bool to_num(const char *str, size_t size, int &num) { - num = 0; - for(size_t i = 0; i < size; ++i) { - const char num_c = str[i] - '0'; - if(num_c < 0 || num_c > 9) - return false; - num = (num * 10) + num_c; - } - return true; - } - static void html_unescape_sequence_numbers(std::string &str) { size_t index = 0; while(true) { @@ -69,10 +58,13 @@ namespace QuickMedia { void html_unescape_sequences(std::string &str) { html_unescape_sequence_numbers(str); - const std::array unescape_sequences = { + // TODO: Use string find and find & and ; instead of string_replace_all + const std::array unescape_sequences = { HtmlUnescapeSequence { """, "\"" }, HtmlUnescapeSequence { "<", "<" }, HtmlUnescapeSequence { ">", ">" }, + HtmlUnescapeSequence { "—", "—" }, + HtmlUnescapeSequence { " ", " " }, HtmlUnescapeSequence { "&", "&" } // This should be last, to not accidentally replace a new sequence caused by replacing this }; diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index d006f3d..9b982a1 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -14,6 +14,7 @@ #include "../plugins/Saucenao.hpp" #include "../plugins/Info.hpp" #include "../plugins/HotExamples.hpp" +#include "../plugins/MyAnimeList.hpp" #include "../include/Scale.hpp" #include "../include/Program.hpp" #include "../include/VideoPlayer.hpp" @@ -77,6 +78,7 @@ static const std::pair valid_plugins[] = { std::make_pair("4chan", "4chan_logo.png"), std::make_pair("nyaa.si", "nyaa_si_logo.png"), std::make_pair("matrix", "matrix_logo.png"), + std::make_pair("mal", "mal_logo.png"), std::make_pair("hotexamples", nullptr), std::make_pair("file-manager", nullptr), std::make_pair("stdin", nullptr), @@ -308,7 +310,7 @@ namespace QuickMedia { return PluginResult::OK; } - bool submit_is_async() override { return false; } + bool submit_is_async() const override { return false; } void add_option(Body *body, std::string title, std::string description, OptionsPageHandler handler) { assert(handler); @@ -349,7 +351,7 @@ namespace QuickMedia { static void usage() { fprintf(stderr, "usage: quickmedia [plugin] [--no-video] [--use-system-mpv-config] [--dir ] [-e ] [youtube-url]\n"); fprintf(stderr, "OPTIONS:\n"); - fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n"); + fprintf(stderr, " plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, mal, file-manager, stdin, pornhub, spankbang, xvideos or xhamster\n"); fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n"); fprintf(stderr, " --use-system-mpv-config Use system mpv config instead of no config. Disabled by default\n"); fprintf(stderr, " --upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default\n"); @@ -1091,6 +1093,7 @@ namespace QuickMedia { pipe_body->set_items({ create_launcher_body_item("4chan", "4chan", resources_root + "icons/4chan_launcher.png"), create_launcher_body_item("Hot Examples", "hotexamples", ""), + create_launcher_body_item("MyAnimeList", "mal", resources_root + "images/mal_logo.png"), create_launcher_body_item("Manga (all)", "manga", ""), create_launcher_body_item("Mangadex", "mangadex", resources_root + "icons/mangadex_launcher.png"), create_launcher_body_item("Mangakatana", "mangakatana", resources_root + "icons/mangakatana_launcher.png"), @@ -1204,6 +1207,8 @@ namespace QuickMedia { hot_examples_front_page_fill(body_items); body->set_items(std::move(body_items)); tabs.push_back(Tab{std::move(body), std::make_unique(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)}); + } else if(strcmp(plugin_name, "mal") == 0) { + tabs.push_back(Tab{create_body(), std::make_unique(this), create_search_bar("Search...", 400)}); } else if(strcmp(plugin_name, "file-manager") == 0) { auto file_manager_page = std::make_unique(this, fm_mime_type, file_selection_handler); if(!file_manager_page->set_current_directory(file_manager_start_dir)) @@ -1685,7 +1690,6 @@ namespace QuickMedia { for(Tab &tab : tabs) { if(tab.body->attach_side == AttachSide::BOTTOM) tab.body->select_last_item(); - tab.body->thumbnail_max_size = tab.page->get_thumbnail_max_size(); tab.page->on_navigate_to_page(tab.body.get()); } diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp index 9820d29..5dfeca9 100644 --- a/src/StringUtils.cpp +++ b/src/StringUtils.cpp @@ -129,4 +129,24 @@ namespace QuickMedia { ++str2; } } + + bool to_num(const char *str, size_t size, int &num) { + size_t i = 0; + const bool is_negative = size > 0 && str[0] == '-'; + if(is_negative) + i = 1; + + num = 0; + for(; i < size; ++i) { + const char num_c = str[i] - '0'; + if(num_c < 0 || num_c > 9) + return false; + num = (num * 10) + num_c; + } + + if(is_negative) + num = -num; + + return true; + } } \ No newline at end of file diff --git a/src/plugins/MangaGeneric.cpp b/src/plugins/MangaGeneric.cpp index 90da0c2..738c6dc 100644 --- a/src/plugins/MangaGeneric.cpp +++ b/src/plugins/MangaGeneric.cpp @@ -104,8 +104,10 @@ namespace QuickMedia { && field_value.data && (!merge_userdata->field_contains || string_view_contains(field_value, merge_userdata->field_contains))) { std::string field_stripped(field_value.data, field_value.size); + html_unescape_sequences(field_stripped); if(merge_userdata->type == MergeType::THUMBNAIL) { (*body_item_image_context.body_items)[body_item_image_context.index]->thumbnail_url = std::move(field_stripped); + (*body_item_image_context.body_items)[body_item_image_context.index]->thumbnail_size = {101, 141}; } else if(merge_userdata->type == MergeType::DESCRIPTION) { const char *prefix = merge_userdata->desc_prefix ? merge_userdata->desc_prefix : ""; (*body_item_image_context.body_items)[body_item_image_context.index]->set_description(prefix + std::move(field_stripped)); diff --git a/src/plugins/Mangadex.cpp b/src/plugins/Mangadex.cpp index 98683c1..5b8debd 100644 --- a/src/plugins/Mangadex.cpp +++ b/src/plugins/Mangadex.cpp @@ -138,6 +138,7 @@ namespace QuickMedia { 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; diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp index d3d7bfa..90003cb 100644 --- a/src/plugins/Manganelo.cpp +++ b/src/plugins/Manganelo.cpp @@ -139,6 +139,7 @@ namespace QuickMedia { Json::Value image = child.get("image", ""); if(image.isString() && image.asCString()[0] != '\0') item->thumbnail_url = image.asString(); + item->thumbnail_size = {101, 141}; result_items.push_back(std::move(item)); } } @@ -228,6 +229,7 @@ namespace QuickMedia { QuickMediaStringView src = quickmedia_html_node_get_attribute_value(node, "src"); if(src.data && item_data->index < item_data->body_items->size()) { (*item_data->body_items)[item_data->index]->thumbnail_url.assign(src.data, src.size); + (*item_data->body_items)[item_data->index]->thumbnail_size = {101, 141}; item_data->index++; } return 0; diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp index fce4135..775d172 100644 --- a/src/plugins/Matrix.cpp +++ b/src/plugins/Matrix.cpp @@ -1173,6 +1173,7 @@ namespace QuickMedia { assert(!this->delegate); assert(!access_token.empty()); // Need to be logged in + assert(delegate); this->delegate = delegate; Path matrix_cache_dir = get_cache_dir().join("matrix"); @@ -2033,13 +2034,11 @@ namespace QuickMedia { } } - if(delegate) { - bool cache_sync = sync_is_cache; - bool is_initial_sync = next_batch.empty(); - ui_thread_tasks.push([this, room_data, cache_sync, new_messages{std::move(new_messages)}, is_initial_sync, message_dir]{ - delegate->room_add_new_messages(room_data, new_messages, is_initial_sync, cache_sync, message_dir); - }); - } + bool cache_sync = sync_is_cache; + bool is_initial_sync = next_batch.empty(); + ui_thread_tasks.push([this, room_data, cache_sync, new_messages{std::move(new_messages)}, is_initial_sync, message_dir]{ + delegate->room_add_new_messages(room_data, new_messages, is_initial_sync, cache_sync, message_dir); + }); return num_new_messages; } diff --git a/src/plugins/MediaGeneric.cpp b/src/plugins/MediaGeneric.cpp index d536a09..bcb8dc3 100644 --- a/src/plugins/MediaGeneric.cpp +++ b/src/plugins/MediaGeneric.cpp @@ -37,7 +37,7 @@ namespace QuickMedia { } } - static PluginResult fetch_page_results(const std::string &url, const std::string &website_url, const std::vector &text_queries, const std::vector &thumbnail_queries, MediaRelatedCustomHandler *custom_handler, BodyItems &result_items, bool cloudflare_bypass) { + static PluginResult fetch_page_results(const std::string &url, const std::string &website_url, const std::vector &text_queries, const std::vector &thumbnail_queries, sf::Vector2i thumbnail_max_size, MediaRelatedCustomHandler *custom_handler, BodyItems &result_items, bool cloudflare_bypass) { std::vector args; if(!website_url.empty()) args.push_back({ "-H", "referer: " + website_url }); @@ -55,6 +55,7 @@ namespace QuickMedia { auto body_item = BodyItem::create(media_related_item.title); body_item->url = std::move(media_related_item.url); body_item->thumbnail_url = std::move(media_related_item.thumbnail_url); + body_item->thumbnail_size = thumbnail_max_size; result_items.push_back(std::move(body_item)); } body_items_prepend_website_url(result_items, website_url); @@ -92,10 +93,11 @@ namespace QuickMedia { assert(thumbnail_query.html_query && thumbnail_query.field_name); if(thumbnail_query.html_query && thumbnail_query.field_name) { size_t index = 0; - result = quickmedia_html_find_nodes_xpath(&html_search, thumbnail_query.html_query, [&thumbnail_query, &result_items, &index](QuickMediaMatchNode *node) { + result = quickmedia_html_find_nodes_xpath(&html_search, thumbnail_query.html_query, [&thumbnail_query, &result_items, &index, thumbnail_max_size](QuickMediaMatchNode *node) { QuickMediaStringView field_value = html_attr_or_inner_text(node, thumbnail_query.field_name); if(index < result_items.size() && field_value.data && (!thumbnail_query.field_contains || string_view_contains(field_value, thumbnail_query.field_contains))) { result_items[index]->thumbnail_url.assign(field_value.data, field_value.size); + result_items[index]->thumbnail_size = thumbnail_max_size; ++index; } }); @@ -133,7 +135,7 @@ namespace QuickMedia { std::string url = search_query.search_template; string_replace_all(url, "%s", url_param_encode(str)); string_replace_all(url, "%p", std::to_string(search_query.page_start + page)); - return fetch_page_results(url, website_url, text_queries, thumbnail_queries, nullptr, result_items, cloudflare_bypass); + return fetch_page_results(url, website_url, text_queries, thumbnail_queries, thumbnail_max_size, nullptr, result_items, cloudflare_bypass); } PluginResult MediaGenericSearchPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { @@ -142,7 +144,7 @@ namespace QuickMedia { } PluginResult MediaGenericSearchPage::get_related_media(const std::string &url, BodyItems &result_items) { - return fetch_page_results(url, website_url, related_media_text_queries, related_media_thumbnail_queries, &related_custom_handler, result_items, cloudflare_bypass); + return fetch_page_results(url, website_url, related_media_text_queries, related_media_thumbnail_queries, thumbnail_max_size, &related_custom_handler, result_items, cloudflare_bypass); } MediaGenericSearchPage& MediaGenericSearchPage::search_handler(const char *search_template, int page_start) { diff --git a/src/plugins/MyAnimeList.cpp b/src/plugins/MyAnimeList.cpp new file mode 100644 index 0000000..dd80297 --- /dev/null +++ b/src/plugins/MyAnimeList.cpp @@ -0,0 +1,311 @@ +#include "../../plugins/MyAnimeList.hpp" +#include "../../include/Theme.hpp" +#include "../../include/NetUtils.hpp" +#include "../../include/StringUtils.hpp" +#include + +namespace QuickMedia { + // Returns {0, 0} if unknown + static sf::Vector2i thumbnail_url_get_resolution(const std::string &url) { + const size_t index = url.find("/r/"); + if(index == std::string::npos) + return {0, 0}; + + const size_t width_index = index + 3; + const size_t x_index = url.find('x', width_index); + if(x_index == std::string::npos) + return {0, 0}; + + const size_t height_index = x_index + 1; + const size_t size_end_index = url.find('/', height_index); + if(size_end_index == std::string::npos) + return {0, 0}; + + sf::Vector2i size; + if(!to_num(url.c_str() + width_index, (x_index - width_index), size.x) || !to_num(url.c_str() + height_index, (size_end_index - height_index), size.y)) + return {0, 0}; + + return size; + } + + static std::shared_ptr search_item_to_body_item(const Json::Value &search_item_json) { + if(!search_item_json.isObject()) + return nullptr; + + const Json::Value &name_json = search_item_json["name"]; + const Json::Value &url_json = search_item_json["url"]; + const Json::Value &payload_json = search_item_json["payload"]; + if(!name_json.isString() || !url_json.isString() || !payload_json.isObject()) + return nullptr; + + std::string name = name_json.asString(); + const Json::Value &media_type_json = payload_json["media_type"]; + if(media_type_json.isString()) + name += " (" + media_type_json.asString() + ")"; + + auto body_item = BodyItem::create(""); + body_item->url = url_json.asString(); + body_item->set_author(std::move(name)); + + const Json::Value &image_url_json = search_item_json["thumbnail_url"]; + body_item->thumbnail_size = {116, 76}; + if(image_url_json.isString()) { + body_item->thumbnail_url = image_url_json.asString(); + body_item->thumbnail_size = thumbnail_url_get_resolution(body_item->thumbnail_url); + } + + std::string description; + const Json::Value &aired_json = payload_json["aired"]; + if(aired_json.isString()) { + if(!description.empty()) + description += '\n'; + description += "Aired: " + aired_json.asString(); + } + + const Json::Value &published_json = payload_json["published"]; + if(published_json.isString()) { + if(!description.empty()) + description += '\n'; + description += "Published: " + published_json.asString(); + } + + const Json::Value &score_json = payload_json["score"]; + if(score_json.isString()) { + if(!description.empty()) + description += '\n'; + description += "Score: " + score_json.asString(); + } + + const Json::Value &status_json = payload_json["status"]; + if(status_json.isString()) { + if(!description.empty()) + description += '\n'; + description += "Status: " + status_json.asString(); + } + + if(!description.empty()) { + body_item->set_description(std::move(description)); + body_item->set_description_color(get_current_theme().faded_text_color); + } + + return body_item; + } + + SearchResult MyAnimeListSearchPage::search(const std::string &str, BodyItems &result_items) { + std::string url = "https://myanimelist.net/search/prefix.json?type=all&keyword="; + url += url_param_encode(str) + "&v=1"; + + Json::Value json_root; + DownloadResult download_result = download_json(json_root, url, {}, true); + if(download_result != DownloadResult::OK) return download_result_to_search_result(download_result); + + if(!json_root.isObject()) + return SearchResult::ERR; + + const Json::Value &categories_json = json_root["categories"]; + if(!categories_json.isArray()) + return SearchResult::ERR; + + BodyItems anime_items; + BodyItems manga_items; + for(const Json::Value &category_json : categories_json) { + if(!category_json.isObject()) + continue; + + const Json::Value &type_json = category_json["type"]; + const Json::Value &items_json = category_json["items"]; + if(!type_json.isString() || !items_json.isArray()) + continue; + + if(strcmp(type_json.asCString(), "anime") == 0) { + for(const Json::Value &item_json : items_json) { + auto body_item = search_item_to_body_item(item_json); + if(body_item) + anime_items.push_back(std::move(body_item)); + } + } else if(strcmp(type_json.asCString(), "manga") == 0) { + for(const Json::Value &item_json : items_json) { + auto body_item = search_item_to_body_item(item_json); + if(body_item) + manga_items.push_back(std::move(body_item)); + } + } + } + + auto anime_title_item = BodyItem::create(""); + anime_title_item->set_author("------------------------ Anime ------------------------"); + result_items.push_back(std::move(anime_title_item)); + result_items.insert(result_items.end(), std::move_iterator(anime_items.begin()), std::move_iterator(anime_items.end())); + + auto manga_title_item = BodyItem::create(""); + manga_title_item->set_author("------------------------ Manga ------------------------"); + result_items.push_back(std::move(manga_title_item)); + result_items.insert(result_items.end(), std::move_iterator(manga_items.begin()), std::move_iterator(manga_items.end())); + + return SearchResult::OK; + } + + PluginResult MyAnimeListSearchPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + if(url.empty()) + return PluginResult::OK; + + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + result_tabs.push_back({ create_body(false, true), std::make_unique(program, url), nullptr }); + return PluginResult::OK; + } + + PluginResult MyAnimeListDetailsPage::submit(const std::string &title, const std::string &url, std::vector &result_tabs) { + return PluginResult::OK; + } + + PluginResult MyAnimeListDetailsPage::lazy_fetch(BodyItems &result_items) { + std::string website_data; + DownloadResult download_result = download_to_string(url, website_data, {}, true); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + std::string thumbnail_url; + std::string description; + + QuickMediaHtmlSearch html_search; + int result = quickmedia_html_search_init(&html_search, website_data.c_str(), website_data.size()); + if(result != 0) + return PluginResult::ERR; + + quickmedia_html_find_nodes_xpath(&html_search, "//img[itemprop='image']", + [](QuickMediaMatchNode *node, void *userdata) { + std::string *thumbnail_url = (std::string*)userdata; + QuickMediaStringView data_src = quickmedia_html_node_get_attribute_value(node, "data-src"); + if(data_src.data) { + thumbnail_url->assign(data_src.data, data_src.size); + html_unescape_sequences(*thumbnail_url); + } + return 1; + }, &thumbnail_url); + + quickmedia_html_find_nodes_xpath(&html_search, "//p[itemprop='description']", + [](QuickMediaMatchNode *node, void *userdata) { + std::string *description = (std::string*)userdata; + QuickMediaStringView text = quickmedia_html_node_get_text(node); + if(text.data) { + *description = "Synopsis:\n"; + description->append(text.data, text.size); + html_unescape_sequences(*description); + } + return 1; + }, &description); + + quickmedia_html_search_deinit(&html_search); + + auto synopsis_body_item = BodyItem::create(""); + synopsis_body_item->set_description(std::move(description)); + synopsis_body_item->thumbnail_url = std::move(thumbnail_url); + synopsis_body_item->thumbnail_size = {225, 337}; + result_items.push_back(std::move(synopsis_body_item)); + + return PluginResult::OK; + } + + PluginResult MyAnimeListRecommendationsPage::submit(const std::string&, const std::string &url, std::vector &result_tabs) { + result_tabs.push_back({ create_body(), std::make_unique(program, url), nullptr }); + result_tabs.push_back({ create_body(false, true), std::make_unique(program, url), nullptr }); + return PluginResult::OK; + } + + static std::string img_alt_get_title(const std::string &alt) { + size_t index = alt.find(':'); + if(index == std::string::npos) + return alt; + return alt.substr(index + 2); + } + + static QuickMediaStringView quickmedia_html_node_get_attribute_value(QuickMediaHtmlNode *node, const char *attribute_name) { + QuickMediaMatchNode match_node; + match_node.node = node; + return quickmedia_html_node_get_attribute_value(&match_node, attribute_name); + } + + PluginResult MyAnimeListRecommendationsPage::lazy_fetch(BodyItems &result_items) { + std::string website_data; + DownloadResult download_result = download_to_string(url + "/userrecs", website_data, {}, true); + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + QuickMediaHtmlSearch html_search; + int result = quickmedia_html_search_init(&html_search, website_data.c_str(), website_data.size()); + if(result != 0) + return PluginResult::ERR; + + quickmedia_html_find_nodes_xpath(&html_search, "//div[class='picSurround']/a", + [](QuickMediaMatchNode *node, void *userdata) { + BodyItems *result_items = (BodyItems*)userdata; + QuickMediaStringView href = quickmedia_html_node_get_attribute_value(node, "href"); + if(!href.data) + return 0; + + auto body_item = BodyItem::create(""); + body_item->url.assign(href.data, href.size); + html_unescape_sequences(body_item->url); + result_items->push_back(body_item); + if(!node->node->first_child || !node->node->first_child->node.name.data || node->node->first_child->node.name.size != 3 || memcmp(node->node->first_child->node.name.data, "img", 3) != 0) + return 0; + + QuickMediaStringView data_src = quickmedia_html_node_get_attribute_value(&node->node->first_child->node, "data-src"); + QuickMediaStringView alt = quickmedia_html_node_get_attribute_value(&node->node->first_child->node, "alt"); + if(data_src.data && alt.data) { + std::string title = img_alt_get_title(std::string(alt.data, alt.size)); + html_unescape_sequences(title); + body_item->set_author(std::move(title)); + + body_item->thumbnail_url.assign(data_src.data, data_src.size); + html_unescape_sequences(body_item->thumbnail_url); + + if(body_item->thumbnail_url.empty()) + body_item->thumbnail_size = {50, 70}; + else + body_item->thumbnail_size = thumbnail_url_get_resolution(body_item->thumbnail_url); + } + return 0; + }, &result_items); + + BodyItemContext body_item_image_context; + body_item_image_context.body_items = &result_items; + body_item_image_context.index = 0; + + // TODO: Fix, incorrect descriptions! + #if 0 + quickmedia_html_find_nodes_xpath(&html_search, "//div[class='*detail-user-recs-text']", + [](QuickMediaMatchNode *node, void *userdata) { + BodyItemContext *item_data = (BodyItemContext*)userdata; + QuickMediaStringView text = quickmedia_html_node_get_text(node); + if(text.data && item_data->index < item_data->body_items->size()) { + std::string description(text.data, text.size); + html_unescape_sequences(description); + (*item_data->body_items)[item_data->index]->set_description(std::move(description)); + (*item_data->body_items)[item_data->index]->set_description_color(get_current_theme().faded_text_color); + item_data->index++; + } + return 0; + }, &body_item_image_context); + #endif + + /* + quickmedia_html_find_nodes_xpath(&html_search, "//div[class='borderClass']//div[class='*detail-user-recs-text']", + [](QuickMediaMatchNode *node, void *userdata) { + BodyItems *result_items = (BodyItems*)userdata; + QuickMediaStringView text = quickmedia_html_node_get_text(node); + if(text.data) { + std::string description(text.data, text.size); + html_unescape_sequences(description); + + auto body_item = BodyItem::create(""); + body_item->set_description(std::move(description)); + body_item->set_description_color(get_current_theme().faded_text_color); + result_items->push_back(std::move(body_item)); + } + return 0; + }, &result_items); + */ + + quickmedia_html_search_deinit(&html_search); + return PluginResult::OK; + } +} \ No newline at end of file -- cgit v1.2.3