From 657edb8eb9ab2fdef60d9c5d23a4c3093a64d859 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Mon, 5 Aug 2019 18:17:00 +0200 Subject: Add manga chapter viewing --- README.md | 5 +- include/Page.hpp | 4 +- include/QuickMedia.hpp | 7 +- plugins/Manganelo.hpp | 8 +- plugins/Plugin.hpp | 10 +- plugins/Youtube.hpp | 2 +- src/Manganelo.cpp | 114 --------------------- src/Program.c | 10 +- src/QuickMedia.cpp | 254 +++++++++++++++++++++++++++++++++------------- src/Youtube.cpp | 118 --------------------- src/plugins/Manganelo.cpp | 146 ++++++++++++++++++++++++++ src/plugins/Youtube.cpp | 120 ++++++++++++++++++++++ 12 files changed, 491 insertions(+), 307 deletions(-) delete mode 100644 src/Manganelo.cpp delete mode 100644 src/Youtube.cpp create mode 100644 src/plugins/Manganelo.cpp create mode 100644 src/plugins/Youtube.cpp diff --git a/README.md b/README.md index e08b0a6..9da4b12 100644 --- a/README.md +++ b/README.md @@ -4,4 +4,7 @@ Native clients of websites with fast access to what you want to see See project.conf \[dependencies]. youtube-dl also needs to be installed to play videos from youtube. # TODO Fix x11 freeze that sometimes happens when playing video. -If a search returns no results, then "No results found for ..." should be shown and navigation should go back to searching with suggestions. \ No newline at end of file +If a search returns no results, then "No results found for ..." should be shown and navigation should go back to searching with suggestions. +Keep track of content that has been viewed so the user can return to where they were last. +For manga, view the next chapter when reaching the end of a chapter. +Make network requests asynchronous to not freeze gui when navigating. Also have loading animation. \ No newline at end of file diff --git a/include/Page.hpp b/include/Page.hpp index ce20971..7b1fc17 100644 --- a/include/Page.hpp +++ b/include/Page.hpp @@ -5,6 +5,8 @@ namespace QuickMedia { EXIT, SEARCH_SUGGESTION, SEARCH_RESULT, - VIDEO_CONTENT + VIDEO_CONTENT, + EPISODE_LIST, + IMAGES }; } \ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index d5839c7..e5f4f2d 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -17,10 +17,12 @@ namespace QuickMedia { ~Program(); void run(); private: - void base_event_handler(sf::Event &event); + void base_event_handler(sf::Event &event, Page previous_page); void search_suggestion_page(); void search_result_page(); void video_content_page(); + void episode_list_page(); + void image_page(); private: sf::RenderWindow window; sf::Vector2f window_size; @@ -29,6 +31,9 @@ namespace QuickMedia { Plugin *current_plugin; std::unique_ptr search_bar; Page current_page; + // TODO: Combine these std::string video_url; + std::string images_url; + int image_index; }; } \ No newline at end of file diff --git a/plugins/Manganelo.hpp b/plugins/Manganelo.hpp index 4ed976f..5e9382e 100644 --- a/plugins/Manganelo.hpp +++ b/plugins/Manganelo.hpp @@ -5,7 +5,13 @@ namespace QuickMedia { class Manganelo : public Plugin { public: - SearchResult search(const std::string &text, std::vector> &result_items) override; + SearchResult search(const std::string &url, std::vector> &result_items, Page &next_page) override; SuggestionResult update_search_suggestions(const std::string &text, std::vector> &result_items) override; + ImageResult get_image_by_index(const std::string &url, int index, std::string &image_data); + private: + ImageResult get_image_urls_for_chapter(const std::string &url, std::vector &urls); + private: + std::string last_chapter_url; + std::vector last_chapter_image_urls; }; } \ No newline at end of file diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp index 09ce09a..818cc5f 100644 --- a/plugins/Plugin.hpp +++ b/plugins/Plugin.hpp @@ -1,5 +1,6 @@ #pragma once +#include "../include/Page.hpp" #include #include #include @@ -35,6 +36,13 @@ namespace QuickMedia { NET_ERR }; + enum class ImageResult { + OK, + END, + ERR, + NET_ERR + }; + struct CommandArg { std::string option; std::string value; @@ -44,7 +52,7 @@ namespace QuickMedia { public: virtual ~Plugin() = default; - virtual SearchResult search(const std::string &text, std::vector> &result_items) = 0; + virtual SearchResult search(const std::string &text, std::vector> &result_items, Page &next_page) = 0; virtual SuggestionResult update_search_suggestions(const std::string &text, std::vector> &result_items); virtual std::vector> get_related_media(const std::string &url); protected: diff --git a/plugins/Youtube.hpp b/plugins/Youtube.hpp index eda8a1f..c342a10 100644 --- a/plugins/Youtube.hpp +++ b/plugins/Youtube.hpp @@ -5,7 +5,7 @@ namespace QuickMedia { class Youtube : public Plugin { public: - SearchResult search(const std::string &text, std::vector> &result_items) override; + SearchResult search(const std::string &text, std::vector> &result_items, Page &next_page) override; SuggestionResult update_search_suggestions(const std::string &text, std::vector> &result_items) override; std::vector> get_related_media(const std::string &url) override; }; diff --git a/src/Manganelo.cpp b/src/Manganelo.cpp deleted file mode 100644 index 1d3929e..0000000 --- a/src/Manganelo.cpp +++ /dev/null @@ -1,114 +0,0 @@ -#include "../plugins/Manganelo.hpp" -#include -#include - -namespace QuickMedia { - SearchResult Manganelo::search(const std::string &text, std::vector> &result_items) { - std::string url = "https://manganelo.com/search/"; - url += url_param_encode(text); - - std::string website_data; - if(download_to_string(url, website_data) != DownloadResult::OK) - return SearchResult::NET_ERR; - - struct ItemData { - std::vector> &result_items; - size_t item_index; - }; - - ItemData item_data = { result_items, 0 }; - - 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, "//h3[class=\"story_name\"]/a", - [](QuickMediaHtmlNode *node, void *userdata) { - ItemData *item_data = (ItemData*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *text = quickmedia_html_node_get_text(node); - if(href && text) { - auto item = std::make_unique(text); - item->url = href; - item_data->result_items.push_back(std::move(item)); - } - }, &item_data); - if (result != 0) - goto cleanup; - - result = quickmedia_html_find_nodes_xpath(&html_search, "//div[class=\"story_item\"]//img", - [](QuickMediaHtmlNode *node, void *userdata) { - ItemData *item_data = (ItemData*)userdata; - const char *src = quickmedia_html_node_get_attribute_value(node, "src"); - if(src && item_data->item_index < item_data->result_items.size()) { - item_data->result_items[item_data->item_index]->thumbnail_url = src; - ++item_data->item_index; - } - }, &item_data); - - cleanup: - quickmedia_html_search_deinit(&html_search); - return result == 0 ? SearchResult::OK : SearchResult::ERR; - } - - // Returns true if changed - static bool remove_html_span(std::string &str) { - size_t open_tag_start = str.find("', open_tag_start + 5); - if(open_tag_end == std::string::npos) - return false; - - str.erase(open_tag_start, open_tag_end - open_tag_start + 1); - - size_t close_tag = str.find(""); - if(close_tag == std::string::npos) - return true; - - str.erase(close_tag, 7); - return true; - } - - SuggestionResult Manganelo::update_search_suggestions(const std::string &text, std::vector> &result_items) { - std::string url = "https://manganelo.com/home_json_search"; - std::string search_term = "searchword="; - search_term += url_param_encode(text); - CommandArg data_arg = { "--data", std::move(search_term) }; - - std::string server_response; - if(download_to_string(url, server_response, {data_arg}) != DownloadResult::OK) - return SuggestionResult::NET_ERR; - - if(server_response.empty()) - return SuggestionResult::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.front(), &server_response.back(), &json_root, &json_errors)) { - fprintf(stderr, "Manganelo suggestions json error: %s\n", json_errors.c_str()); - return SuggestionResult::ERR; - } - - if(json_root.isArray()) { - for(const Json::Value &child : json_root) { - if(child.isObject()) { - Json::Value name = child.get("name", ""); - if(name.isString() && name.asCString()[0] != '\0') { - std::string name_str = name.asString(); - while(remove_html_span(name_str)) {} - if(name_str != text) { - auto item = std::make_unique(name_str); - result_items.push_back(std::move(item)); - } - } - } - } - } - return SuggestionResult::OK; - } -} \ No newline at end of file diff --git a/src/Program.c b/src/Program.c index 39957ed..38a602f 100644 --- a/src/Program.c +++ b/src/Program.c @@ -66,7 +66,15 @@ int exec_program(const char **args, ProgramOutputCallback output_callback, void int exit_status = WEXITSTATUS(status); if(exit_status != 0) { - fprintf(stderr, "Failed to execute program, exit status %d\n", exit_status); + fprintf(stderr, "Failed to execute program ("); + const char **arg = args; + while(*arg) { + if(arg != args) + fputc(' ', stderr); + fprintf(stderr, "'%s'", *arg); + ++arg; + } + fprintf(stderr, "), exit status %d\n", exit_status); result = -exit_status; goto cleanup; } diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index c7d89e2..e6c7077 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -173,7 +173,8 @@ namespace QuickMedia { window_size(800, 600), body(nullptr), current_plugin(nullptr), - current_page(Page::SEARCH_SUGGESTION) + current_page(Page::SEARCH_SUGGESTION), + image_index(0) { window.setVerticalSyncEnabled(true); if(!font.loadFromFile("fonts/Lato-Regular.ttf")) { @@ -181,8 +182,8 @@ namespace QuickMedia { abort(); } body = new Body(font); - //current_plugin = new Manganelo(); - current_plugin = new Youtube(); + current_plugin = new Manganelo(); + //current_plugin = new Youtube(); search_bar = std::make_unique(font); } @@ -191,14 +192,15 @@ namespace QuickMedia { delete current_plugin; } - static SearchResult search_selected_suggestion(Body *body, Plugin *plugin) { + static SearchResult search_selected_suggestion(Body *body, Plugin *plugin, Page &next_page) { BodyItem *selected_item = body->get_selected(); if(!selected_item) return SearchResult::ERR; std::string selected_item_title = selected_item->title; + std::string selected_item_url = selected_item->url; body->clear_items(); - SearchResult search_result = plugin->search(selected_item_title, body->items); + SearchResult search_result = plugin->search(!selected_item_url.empty() ? selected_item_url : selected_item_title, body->items, next_page); body->reset_selected(); return search_result; } @@ -208,7 +210,6 @@ namespace QuickMedia { if(text.isEmpty()) return; - body->items.push_back(std::make_unique(text)); SuggestionResult suggestion_result = plugin->update_search_suggestions(text, body->items); body->clamp_selection(); } @@ -217,7 +218,8 @@ namespace QuickMedia { while(window.isOpen()) { switch(current_page) { case Page::EXIT: - return; + window.close(); + break; case Page::SEARCH_SUGGESTION: search_suggestion_page(); break; @@ -227,14 +229,40 @@ namespace QuickMedia { case Page::VIDEO_CONTENT: video_content_page(); break; + case Page::EPISODE_LIST: + episode_list_page(); + break; + case Page::IMAGES: + image_page(); + break; default: return; } } } - void Program::base_event_handler(sf::Event &event) { - + void Program::base_event_handler(sf::Event &event, Page previous_page) { + if (event.type == sf::Event::Closed) { + current_page = Page::EXIT; + } else if(event.type == sf::Event::Resized) { + window_size.x = event.size.width; + window_size.y = event.size.height; + sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); + window.setView(sf::View(visible_area)); + } else if(event.type == sf::Event::KeyPressed) { + if(event.key.code == sf::Keyboard::Up) { + body->select_previous_item(); + } else if(event.key.code == sf::Keyboard::Down) { + body->select_next_item(); + } else if(event.key.code == sf::Keyboard::Escape) { + current_page = previous_page; + body->clear_items(); + body->reset_selected(); + search_bar->clear(); + } + } else if(event.type == sf::Event::TextEntered) { + search_bar->onTextEntered(event.text.unicode); + } } void Program::search_suggestion_page() { @@ -243,8 +271,9 @@ namespace QuickMedia { }; search_bar->onTextSubmitCallback = [this](const std::string &text) { - if(search_selected_suggestion(body, current_plugin) == SearchResult::OK) - current_page = Page::SEARCH_RESULT; + Page next_page; + if(search_selected_suggestion(body, current_plugin, next_page) == SearchResult::OK) + current_page = next_page; }; sf::Vector2f body_pos; @@ -252,29 +281,11 @@ namespace QuickMedia { bool resized = true; sf::Event event; - while (window.isOpen() && current_page == Page::SEARCH_SUGGESTION) { + while (current_page == Page::SEARCH_SUGGESTION) { while (window.pollEvent(event)) { - if (event.type == sf::Event::Closed) { - window.close(); - current_page = Page::EXIT; - } else if(event.type == sf::Event::Resized) { - window_size.x = event.size.width; - window_size.y = event.size.height; - sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); - window.setView(sf::View(visible_area)); + base_event_handler(event, Page::EXIT); + if(event.type == sf::Event::Resized) resized = true; - } else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Up) { - body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::Down) { - body->select_next_item(); - } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::EXIT; - window.close(); - } - } else if(event.type == sf::Event::TextEntered) { - search_bar->onTextEntered(event.text.unicode); - } } if(resized) { @@ -321,31 +332,9 @@ namespace QuickMedia { bool resized = true; sf::Event event; - while (window.isOpen() && current_page == Page::SEARCH_RESULT) { + while (current_page == Page::SEARCH_RESULT) { while (window.pollEvent(event)) { - if (event.type == sf::Event::Closed) { - window.close(); - current_page = Page::EXIT; - } else if(event.type == sf::Event::Resized) { - window_size.x = event.size.width; - window_size.y = event.size.height; - sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); - window.setView(sf::View(visible_area)); - resized = true; - } else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Up) { - body->select_previous_item(); - } else if(event.key.code == sf::Keyboard::Down) { - body->select_next_item(); - } else if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::SEARCH_SUGGESTION; - body->clear_items(); - body->reset_selected(); - search_bar->clear(); - } - } else if(event.type == sf::Event::TextEntered) { - search_bar->onTextEntered(event.text.unicode); - } + base_event_handler(event, Page::SEARCH_SUGGESTION); } if(resized) { @@ -400,31 +389,160 @@ namespace QuickMedia { sf::Clock resize_timer; sf::Event event; - while (window.isOpen() && current_page == Page::VIDEO_CONTENT) { + while (current_page == Page::VIDEO_CONTENT) { + while (window.pollEvent(event)) { + base_event_handler(event, Page::SEARCH_SUGGESTION); + if(event.type == sf::Event::Resized) { + if(video_player) + video_player->resize(sf::Vector2i(window_size.x, window_size.y)); + } + } + + window.clear(); + if(video_player) + video_player->draw(window); + window.display(); + } + } + + void Program::episode_list_page() { + search_bar->onTextUpdateCallback = [this](const std::string &text) { + body->filter_search_fuzzy(text); + body->clamp_selection(); + }; + + search_bar->onTextSubmitCallback = [this](const std::string &text) { + BodyItem *selected_item = body->get_selected(); + if(!selected_item) + return; + + images_url = selected_item->url; + image_index = 0; + current_page = Page::IMAGES; + }; + + sf::Vector2f body_pos; + sf::Vector2f body_size; + bool resized = true; + sf::Event event; + + while (current_page == Page::EPISODE_LIST) { + while (window.pollEvent(event)) { + base_event_handler(event, Page::SEARCH_SUGGESTION); + if(event.type == sf::Event::Resized) + resized = true; + } + + if(resized) { + search_bar->onWindowResize(window_size); + + float body_padding_horizontal = 50.0f; + float body_padding_vertical = 50.0f; + float body_width = window_size.x - body_padding_horizontal * 2.0f; + if(body_width < 400) { + body_width = window_size.x; + body_padding_horizontal = 0.0f; + } + + float search_bottom = search_bar->getBottom(); + body_pos = sf::Vector2f(body_padding_horizontal, search_bottom + body_padding_vertical); + body_size = sf::Vector2f(body_width, window_size.y); + } + + search_bar->update(); + + window.clear(back_color); + body->draw(window, body_pos, body_size); + search_bar->draw(window); + window.display(); + } + } + + void Program::image_page() { + search_bar->onTextUpdateCallback = nullptr; + search_bar->onTextSubmitCallback = nullptr; + + sf::Texture image_texture; + sf::Sprite image; + sf::Text error_message("", font, 30); + error_message.setFillColor(sf::Color::White); + + Manganelo *image_plugin = static_cast(current_plugin); + std::string image_data; + + // TODO: Optimize this somehow. One image alone uses more than 20mb ram! Total ram usage for viewing one image + // becomes 40mb (private memory, almost 100mb in total!) Unacceptable! + ImageResult image_result = image_plugin->get_image_by_index(images_url, image_index, image_data); + if(image_result == ImageResult::OK) { + if(image_texture.loadFromMemory(image_data.data(), image_data.size())) { + image_texture.setSmooth(true); + image.setTexture(image_texture, true); + } else { + error_message.setString(std::string("Failed to load image for page ") + std::to_string(image_index)); + } + } else if(image_result == ImageResult::END) { + // TODO: Better error message, with chapter name + error_message.setString("End of chapter"); + } else { + // TODO: Convert ImageResult error to a string and show to user + error_message.setString(std::string("Network error, failed to get image for page ") + std::to_string(image_index)); + } + image_data.resize(0); + + bool error = !error_message.getString().isEmpty(); + bool resized = true; + sf::Event event; + + // TODO: Show current page / number of pages. + // TODO: Show to user if a certain page is missing (by checking page name (number) and checking if some is skipped) + while (current_page == Page::IMAGES) { while (window.pollEvent(event)) { if (event.type == sf::Event::Closed) { - window.close(); current_page = Page::EXIT; } else if(event.type == sf::Event::Resized) { window_size.x = event.size.width; window_size.y = event.size.height; sf::FloatRect visible_area(0, 0, window_size.x, window_size.y); window.setView(sf::View(visible_area)); - if(video_player) - video_player->resize(sf::Vector2i(window_size.x, window_size.y)); + resized = true; } else if(event.type == sf::Event::KeyPressed) { - if(event.key.code == sf::Keyboard::Escape) { - current_page = Page::SEARCH_SUGGESTION; - body->clear_items(); - body->reset_selected(); - search_bar->clear(); + if(event.key.code == sf::Keyboard::Up) { + if(image_index > 0) { + --image_index; + return; + } + } else if(event.key.code == sf::Keyboard::Down) { + if(!error) { + ++image_index; + return; + } + } else if(event.key.code == sf::Keyboard::Escape) { + current_page = Page::EPISODE_LIST; } } } - window.clear(); - if(video_player) - video_player->draw(window); + if(resized) { + if(error) { + auto bounds = error_message.getLocalBounds(); + error_message.setPosition(window_size.x * 0.5f - bounds.width * 0.5f, window_size.y * 0.5f - bounds.height); + } else { + auto texture_size = image.getTexture()->getSize(); + auto image_scale = image.getScale(); + auto image_size = sf::Vector2f(texture_size.x, texture_size.y); + image_size.x *= image_scale.x; + image_size.y *= image_scale.y; + + image.setPosition(window_size.x * 0.5f - image_size.x * 0.5f, window_size.y * 0.5f - image_size.y * 0.5f); + } + } + + window.clear(back_color); + if(error) { + window.draw(error_message); + } else { + window.draw(image); + } window.display(); } } diff --git a/src/Youtube.cpp b/src/Youtube.cpp deleted file mode 100644 index 780244c..0000000 --- a/src/Youtube.cpp +++ /dev/null @@ -1,118 +0,0 @@ -#include "../plugins/Youtube.hpp" -#include -#include -#include - -namespace QuickMedia { - static bool begins_with(const char *str, const char *begin_with) { - return strncmp(str, begin_with, strlen(begin_with)) == 0; - } - - SearchResult Youtube::search(const std::string &text, std::vector> &result_items) { - std::string url = "https://youtube.com/results?search_query="; - url += url_param_encode(text); - - std::string website_data; - if(download_to_string(url, website_data) != 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, "//h3[class=\"yt-lockup-title\"]/a", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *result_items = (std::vector>*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - const char *title = quickmedia_html_node_get_attribute_value(node, "title"); - // Checking for watch?v helps skipping ads - if(href && title && begins_with(href, "/watch?v=")) { - auto item = std::make_unique(title); - item->url = std::string("https://www.youtube.com") + href; - result_items->push_back(std::move(item)); - } - }, &result_items); - - cleanup: - quickmedia_html_search_deinit(&html_search); - return result == 0 ? SearchResult::OK : SearchResult::ERR; - } - - static void iterate_suggestion_result(const Json::Value &value, const std::string &search_text, std::vector> &result_items) { - if(value.isArray()) { - for(const Json::Value &child : value) { - iterate_suggestion_result(child, search_text, result_items); - } - } else if(value.isString()) { - std::string title = value.asString(); - if(title != search_text) { - auto item = std::make_unique(title); - result_items.push_back(std::move(item)); - } - } - } - - SuggestionResult Youtube::update_search_suggestions(const std::string &text, std::vector> &result_items) { - std::string url = "https://clients1.google.com/complete/search?client=youtube&hl=en&gl=us&q="; - url += url_param_encode(text); - - std::string server_response; - if(download_to_string(url, server_response) != DownloadResult::OK) - return SuggestionResult::NET_ERR; - - size_t json_start = server_response.find_first_of('('); - if(json_start == std::string::npos) - return SuggestionResult::ERR; - ++json_start; - - size_t json_end = server_response.find_last_of(')'); - if(json_end == std::string::npos) - return SuggestionResult::ERR; - - if(json_end == 0 || json_start >= json_end) - return SuggestionResult::ERR; - --json_end; - - 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[json_start], &server_response[json_end], &json_root, &json_errors)) { - fprintf(stderr, "Youtube suggestions json error: %s\n", json_errors.c_str()); - return SuggestionResult::ERR; - } - - iterate_suggestion_result(json_root, text, result_items); - return SuggestionResult::OK; - } - - std::vector> Youtube::get_related_media(const std::string &url) { - std::vector> result_items; - - std::string website_data; - if(download_to_string(url, website_data) != DownloadResult::OK) - return result_items; - - 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, "//ul[class=\"video-list\"]//div[class=\"content-wrapper\"]/a", - [](QuickMediaHtmlNode *node, void *userdata) { - auto *result_items = (std::vector>*)userdata; - const char *href = quickmedia_html_node_get_attribute_value(node, "href"); - // TODO: Also add title for related media - if(href && begins_with(href, "/watch?v=")) { - auto item = std::make_unique(""); - item->url = std::string("https://www.youtube.com") + href; - result_items->push_back(std::move(item)); - } - }, &result_items); - - cleanup: - quickmedia_html_search_deinit(&html_search); - return result_items; - } -} \ No newline at end of file diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp new file mode 100644 index 0000000..bc99387 --- /dev/null +++ b/src/plugins/Manganelo.cpp @@ -0,0 +1,146 @@ +#include "../../plugins/Manganelo.hpp" +#include +#include + +namespace QuickMedia { + SearchResult Manganelo::search(const std::string &url, std::vector> &result_items, Page &next_page) { + next_page = Page::EPISODE_LIST; + + std::string website_data; + if(download_to_string(url, website_data) != 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, "//div[class='chapter-list']/div[class='row']//a", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *item_data = (std::vector>*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + const char *text = quickmedia_html_node_get_text(node); + if(href && text) { + auto item = std::make_unique(text); + item->url = href; + item_data->push_back(std::move(item)); + } + }, &result_items); + + cleanup: + quickmedia_html_search_deinit(&html_search); + return result == 0 ? SearchResult::OK : SearchResult::ERR; + } + + // Returns true if changed + static bool remove_html_span(std::string &str) { + size_t open_tag_start = str.find("', open_tag_start + 5); + if(open_tag_end == std::string::npos) + return false; + + str.erase(open_tag_start, open_tag_end - open_tag_start + 1); + + size_t close_tag = str.find(""); + if(close_tag == std::string::npos) + return true; + + str.erase(close_tag, 7); + return true; + } + + SuggestionResult Manganelo::update_search_suggestions(const std::string &text, std::vector> &result_items) { + std::string url = "https://manganelo.com/home_json_search"; + std::string search_term = "searchword="; + search_term += url_param_encode(text); + CommandArg data_arg = { "--data", std::move(search_term) }; + + std::string server_response; + if(download_to_string(url, server_response, {data_arg}) != DownloadResult::OK) + return SuggestionResult::NET_ERR; + + if(server_response.empty()) + return SuggestionResult::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.front(), &server_response.back(), &json_root, &json_errors)) { + fprintf(stderr, "Manganelo suggestions json error: %s\n", json_errors.c_str()); + return SuggestionResult::ERR; + } + + if(json_root.isArray()) { + for(const Json::Value &child : json_root) { + if(child.isObject()) { + Json::Value name = child.get("name", ""); + Json::Value nameunsigned = child.get("nameunsigned", ""); + if(name.isString() && name.asCString()[0] != '\0' && nameunsigned.isString() && nameunsigned.asCString()[0] != '\0') { + std::string name_str = name.asString(); + while(remove_html_span(name_str)) {} + if(name_str != text) { + auto item = std::make_unique(name_str); + item->url = "https://manganelo.com/manga/" + url_param_encode(nameunsigned.asString()); + result_items.push_back(std::move(item)); + } + } + } + } + } + return SuggestionResult::OK; + } + + ImageResult Manganelo::get_image_by_index(const std::string &url, int index, std::string &image_data) { + if(url != last_chapter_url) { + printf("Get list of image urls for chapter: %s\n", url.c_str()); + last_chapter_image_urls.clear(); + ImageResult image_result = get_image_urls_for_chapter(url, last_chapter_image_urls); + if(image_result != ImageResult::OK) + return image_result; + last_chapter_url = url; + } + + int num_images = last_chapter_image_urls.size(); + if(index < 0 || index >= num_images) + return ImageResult::END; + + // TODO: Cache image in file/memory + switch(download_to_string(last_chapter_image_urls[index], image_data)) { + case DownloadResult::OK: + return ImageResult::OK; + case DownloadResult::ERR: + return ImageResult::ERR; + case DownloadResult::NET_ERR: + return ImageResult::NET_ERR; + default: + return ImageResult::ERR; + } + } + + ImageResult Manganelo::get_image_urls_for_chapter(const std::string &url, std::vector &urls) { + std::string website_data; + if(download_to_string(url, website_data) != DownloadResult::OK) + return ImageResult::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, "//div[id='vungdoc']/img", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *urls = (std::vector*)userdata; + const char *src = quickmedia_html_node_get_attribute_value(node, "src"); + if(src) + urls->push_back(src); + }, &urls); + + cleanup: + quickmedia_html_search_deinit(&html_search); + return result == 0 ? ImageResult::OK : ImageResult::ERR; + } +} \ No newline at end of file diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp new file mode 100644 index 0000000..6cc4ac6 --- /dev/null +++ b/src/plugins/Youtube.cpp @@ -0,0 +1,120 @@ +#include "../../plugins/Youtube.hpp" +#include +#include +#include + +namespace QuickMedia { + static bool begins_with(const char *str, const char *begin_with) { + return strncmp(str, begin_with, strlen(begin_with)) == 0; + } + + SearchResult Youtube::search(const std::string &text, std::vector> &result_items, Page &next_page) { + next_page = Page::SEARCH_RESULT; + std::string url = "https://youtube.com/results?search_query="; + url += url_param_encode(text); + + std::string website_data; + if(download_to_string(url, website_data) != 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, "//h3[class=\"yt-lockup-title\"]/a", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *result_items = (std::vector>*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + const char *title = quickmedia_html_node_get_attribute_value(node, "title"); + // Checking for watch?v helps skipping ads + if(href && title && begins_with(href, "/watch?v=")) { + auto item = std::make_unique(title); + item->url = std::string("https://www.youtube.com") + href; + result_items->push_back(std::move(item)); + } + }, &result_items); + + cleanup: + quickmedia_html_search_deinit(&html_search); + return result == 0 ? SearchResult::OK : SearchResult::ERR; + } + + static void iterate_suggestion_result(const Json::Value &value, const std::string &search_text, std::vector> &result_items) { + if(value.isArray()) { + for(const Json::Value &child : value) { + iterate_suggestion_result(child, search_text, result_items); + } + } else if(value.isString()) { + std::string title = value.asString(); + if(title != search_text) { + auto item = std::make_unique(title); + result_items.push_back(std::move(item)); + } + } + } + + SuggestionResult Youtube::update_search_suggestions(const std::string &text, std::vector> &result_items) { + result_items.push_back(std::make_unique(text)); + std::string url = "https://clients1.google.com/complete/search?client=youtube&hl=en&gl=us&q="; + url += url_param_encode(text); + + std::string server_response; + if(download_to_string(url, server_response) != DownloadResult::OK) + return SuggestionResult::NET_ERR; + + size_t json_start = server_response.find_first_of('('); + if(json_start == std::string::npos) + return SuggestionResult::ERR; + ++json_start; + + size_t json_end = server_response.find_last_of(')'); + if(json_end == std::string::npos) + return SuggestionResult::ERR; + + if(json_end == 0 || json_start >= json_end) + return SuggestionResult::ERR; + --json_end; + + 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[json_start], &server_response[json_end], &json_root, &json_errors)) { + fprintf(stderr, "Youtube suggestions json error: %s\n", json_errors.c_str()); + return SuggestionResult::ERR; + } + + iterate_suggestion_result(json_root, text, result_items); + return SuggestionResult::OK; + } + + std::vector> Youtube::get_related_media(const std::string &url) { + std::vector> result_items; + + std::string website_data; + if(download_to_string(url, website_data) != DownloadResult::OK) + return result_items; + + 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, "//ul[class=\"video-list\"]//div[class=\"content-wrapper\"]/a", + [](QuickMediaHtmlNode *node, void *userdata) { + auto *result_items = (std::vector>*)userdata; + const char *href = quickmedia_html_node_get_attribute_value(node, "href"); + // TODO: Also add title for related media + if(href && begins_with(href, "/watch?v=")) { + auto item = std::make_unique(""); + item->url = std::string("https://www.youtube.com") + href; + result_items->push_back(std::move(item)); + } + }, &result_items); + + cleanup: + quickmedia_html_search_deinit(&html_search); + return result_items; + } +} \ No newline at end of file -- cgit v1.2.3