From e67b9899feb72027b246e3b63ce5aa0ccae2dd16 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sat, 30 Nov 2019 20:23:07 +0100 Subject: Add image board (4chan) comment navigation --- README.md | 15 ++- include/Body.hpp | 2 + include/DataView.hpp | 10 ++ include/Page.hpp | 4 +- include/QuickMedia.hpp | 3 + plugins/Fourchan.hpp | 12 +- plugins/ImageBoard.hpp | 16 +++ plugins/Plugin.hpp | 2 + src/Body.cpp | 6 +- src/QuickMedia.cpp | 186 +++++++++++++++++++++++++++++ src/plugins/Fourchan.cpp | 304 +++++++++++++++++++++++++++++------------------ 11 files changed, 428 insertions(+), 132 deletions(-) create mode 100644 include/DataView.hpp create mode 100644 plugins/ImageBoard.hpp diff --git a/README.md b/README.md index e452ca3..ec7a201 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # QuickMedia -Native clients of websites with fast access to what you want to see, **with TOR support**. See [old video demo with manga](https://beta.lbry.tv/quickmedia_manga-2019-08-05_21.20.46/7).\ -Currently supported websites: `youtube`, `manganelo` and _others_.\ +Native clients of websites with fast access to what you want to see, **with TOR support**. See [old video demo with manga](https://lbry.tv/quickmedia_manga-2019-08-05_21.20.46/7).\ +Currently supported websites: `youtube`, `manganelo`, `4chan` and _others_.\ **Note:** Manganelo doesn't work when used with TOR.\ **Note:** TOR system service needs to be running (`systemctl start tor.service`).\ Here is an example with YouTube:\ @@ -10,7 +10,7 @@ Config data, including manga progress is stored under `$HOME/.config/quickmedia` ``` usage: QuickMedia [--tor] OPTIONS: -plugin The plugin to use. Should be either 4chan, manganelo, pornhub or youtube +plugin The plugin to use. Should be either 4chan, manganelo or youtube --tor Use tor. Disabled by default EXAMPLES: QuickMedia manganelo @@ -18,9 +18,11 @@ QuickMedia youtube --tor ``` ## Controls Press `arrow up` and `arrow down` to navigate the menu and also to go to the previous/next image when viewing manga.\ +Press `Return` (aka `Enter`) to select the item.\ Press `ESC` to go back to the previous menu.\ Press `Ctrl + T` when hovering over a manga chapter to start tracking manga after that chapter. This only works if AutoMedia is installed and -accessible in PATH environment variable. +accessible in PATH environment variable.\ +Press `Backspace` to return to the preview item when reading replies in image board threads. ## Video controls Press `space` to pause/unpause video. `Double-click` video to fullscreen or leave fullscreen. # Dependencies @@ -50,4 +52,7 @@ Somehow deal with youtube banning ip when searching too often.\ Optimize shadow rendering for items (Right now they fill too much space that is behind items). It should also be a blurry shadow.\ When continuing to read manga from a different page from the first and there is no cache for the chapter, then start downloading from the current page instead of page 1.\ -Show progress of manga in the history tab (current chapter out of total chapters). +Show progress of manga in the history tab (current chapter out of total chapters).\ +Animate page navigation.\ +Properly format text in items. For example for 4chan. The size of the item should also change.\ +Show list of replies to a comment (for image boards). diff --git a/include/Body.hpp b/include/Body.hpp index ae1c3ff..32e19dc 100644 --- a/include/Body.hpp +++ b/include/Body.hpp @@ -20,6 +20,8 @@ namespace QuickMedia { std::string url; std::string thumbnail_url; bool visible; + // Used by image boards for example. The elements are indices to other body items + std::vector replies; }; using BodyItems = std::vector>; diff --git a/include/DataView.hpp b/include/DataView.hpp new file mode 100644 index 0000000..baf64dc --- /dev/null +++ b/include/DataView.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include + +namespace QuickMedia { + struct DataView { + char *data; + size_t size; + }; +} \ No newline at end of file diff --git a/include/Page.hpp b/include/Page.hpp index b159877..b615c8c 100644 --- a/include/Page.hpp +++ b/include/Page.hpp @@ -9,6 +9,8 @@ namespace QuickMedia { EPISODE_LIST, IMAGES, CONTENT_LIST, - CONTENT_DETAILS + CONTENT_DETAILS, + IMAGE_BOARD_THREAD_LIST, + IMAGE_BOARD_THREAD }; } \ No newline at end of file diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp index 7bf8231..f4cb250 100644 --- a/include/QuickMedia.hpp +++ b/include/QuickMedia.hpp @@ -32,6 +32,8 @@ namespace QuickMedia { void image_page(); void content_list_page(); void content_details_page(); + void image_board_thread_list_page(); + void image_board_thread_page(); enum class LoadImageResult { OK, @@ -57,6 +59,7 @@ namespace QuickMedia { std::string content_episode; std::string content_url; std::string content_list_url; + std::string image_board_thread_list_url; std::string chapter_title; int image_index; Path content_storage_file; diff --git a/plugins/Fourchan.hpp b/plugins/Fourchan.hpp index 7f9ad13..5011cd9 100644 --- a/plugins/Fourchan.hpp +++ b/plugins/Fourchan.hpp @@ -1,19 +1,19 @@ #pragma once -#include "Plugin.hpp" +#include "ImageBoard.hpp" namespace QuickMedia { - class Fourchan : public Plugin { + class Fourchan : public ImageBoard { public: - Fourchan() : Plugin("4chan") {} + Fourchan() : ImageBoard("4chan") {} PluginResult get_front_page(BodyItems &result_items) override; SearchResult search(const std::string &url, BodyItems &result_items) override; SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override; - PluginResult get_content_list(const std::string &url, BodyItems &result_items) override; - PluginResult get_content_details(const std::string &list_url, const std::string &url, BodyItems &result_items) override; + PluginResult get_threads(const std::string &url, BodyItems &result_items) override; + PluginResult get_thread_comments(const std::string &list_url, const std::string &url, BodyItems &result_items) override; bool search_suggestions_has_thumbnails() const override { return false; } bool search_results_has_thumbnails() const override { return false; } int get_search_delay() const override { return 150; } - Page get_page_after_search() const override { return Page::CONTENT_LIST; } + Page get_page_after_search() const override { return Page::IMAGE_BOARD_THREAD_LIST; } }; } \ No newline at end of file diff --git a/plugins/ImageBoard.hpp b/plugins/ImageBoard.hpp new file mode 100644 index 0000000..090f775 --- /dev/null +++ b/plugins/ImageBoard.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "Plugin.hpp" + +namespace QuickMedia { + class ImageBoard : public Plugin { + public: + ImageBoard(const std::string &name) : Plugin(name) {} + virtual ~ImageBoard() = default; + + bool is_image_board() override { return true; } + + virtual PluginResult get_threads(const std::string &url, BodyItems &result_items) = 0; + virtual PluginResult get_thread_comments(const std::string &list_url, const std::string &url, BodyItems &result_items) = 0; + }; +} \ No newline at end of file diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp index cfa235c..47f0864 100644 --- a/plugins/Plugin.hpp +++ b/plugins/Plugin.hpp @@ -52,6 +52,8 @@ namespace QuickMedia { Plugin(const std::string &name) : name(name) {} virtual ~Plugin() = default; + virtual bool is_image_board() { return false; } + virtual PluginResult get_front_page(BodyItems &result_items) { (void)result_items; return PluginResult::OK; } diff --git a/src/Body.cpp b/src/Body.cpp index 4976a1f..078c4fc 100644 --- a/src/Body.cpp +++ b/src/Body.cpp @@ -104,8 +104,10 @@ namespace QuickMedia { thumbnail_load_thread = std::thread([this, result, url]() { std::string texture_data; if(program->get_current_plugin()->download_to_string(url, texture_data) == DownloadResult::OK) { - if(result->loadFromMemory(texture_data.data(), texture_data.size())) - result->generateMipmap(); + if(result->loadFromMemory(texture_data.data(), texture_data.size())) { + //result->generateMipmap(); + result->setSmooth(true); + } } loading_thumbnail = false; }); diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index c85f0c0..aea8d60 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -209,7 +209,18 @@ namespace QuickMedia { content_details_page(); break; } + case Page::IMAGE_BOARD_THREAD_LIST: { + body->draw_thumbnails = true; + image_board_thread_list_page(); + break; + } + case Page::IMAGE_BOARD_THREAD: { + body->draw_thumbnails = true; + image_board_thread_page(); + break; + } default: + fprintf(stderr, "Page not implemented: %d\n", current_page); window.close(); break; } @@ -420,6 +431,8 @@ namespace QuickMedia { next_page = Page::SEARCH_RESULT; } else if(next_page == Page::CONTENT_LIST) { content_list_url = content_url; + } else if(next_page == Page::IMAGE_BOARD_THREAD_LIST) { + image_board_thread_list_url = content_url; } current_page = next_page; return true; @@ -1306,6 +1319,71 @@ namespace QuickMedia { sf::Event event; while (current_page == Page::CONTENT_DETAILS) { + while (window.pollEvent(event)) { + base_event_handler(event, Page::CONTENT_LIST); + if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) + redraw = true; + } + + // TODO: This code is duplicated in many places. Handle it in one place. + if(redraw) { + redraw = false; + 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_bottom); + } + + search_bar->update(); + + window.clear(back_color); + body->draw(window, body_pos, body_size); + search_bar->draw(window); + window.display(); + } + } + + void Program::image_board_thread_list_page() { + assert(current_plugin->is_image_board()); + ImageBoard *image_board = static_cast(current_plugin); + if(image_board->get_threads(image_board_thread_list_url, body->items) != PluginResult::OK) { + show_notification("Content list", "Failed to get threads for url: " + image_board_thread_list_url, Urgency::CRITICAL); + current_page = Page::SEARCH_SUGGESTION; + return; + } + + search_bar->onTextUpdateCallback = [this](const std::string &text) { + body->filter_search_fuzzy(text); + body->select_first_item(); + }; + + search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool { + BodyItem *selected_item = body->get_selected(); + if(!selected_item) + return false; + + content_episode = selected_item->title; + content_url = selected_item->url; + current_page = Page::IMAGE_BOARD_THREAD; + body->clear_items(); + return true; + }; + + sf::Vector2f body_pos; + sf::Vector2f body_size; + bool redraw = true; + sf::Event event; + + while (current_page == Page::IMAGE_BOARD_THREAD_LIST) { while (window.pollEvent(event)) { base_event_handler(event, Page::SEARCH_SUGGESTION); if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) @@ -1338,4 +1416,112 @@ namespace QuickMedia { window.display(); } } + + void Program::image_board_thread_page() { + assert(current_plugin->is_image_board()); + ImageBoard *image_board = static_cast(current_plugin); + if(image_board->get_thread_comments(image_board_thread_list_url, content_url, body->items) != PluginResult::OK) { + show_notification("Content details", "Failed to get content details for url: " + content_url, Urgency::CRITICAL); + // TODO: This will return to an empty content list. + // Each page should have its own @Body so we can return to the last page and still have the data loaded + // however the cached images should be cleared. + current_page = Page::IMAGE_BOARD_THREAD_LIST; + return; + } + + // Instead of using search bar to searching, use it for commenting. + // TODO: Have an option for the search bar to be multi-line. + search_bar->onTextUpdateCallback = nullptr; + search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool { + if(text.empty()) + return false; + + return true; + }; + + sf::Vector2f body_pos; + sf::Vector2f body_size; + bool redraw = true; + sf::Event event; + + std::stack comment_navigation_stack; + comment_navigation_stack.push(body); + + while (current_page == Page::IMAGE_BOARD_THREAD) { + while (window.pollEvent(event)) { + 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)); + } + + if(event.type == sf::Event::Resized || event.type == sf::Event::GainedFocus) + redraw = true; + else if(event.type == sf::Event::KeyPressed) { + Body *current_body = comment_navigation_stack.top(); + if(event.key.code == sf::Keyboard::Up) { + current_body->select_previous_item(); + } else if(event.key.code == sf::Keyboard::Down) { + current_body->select_next_item(); + } else if(event.key.code == sf::Keyboard::Escape) { + current_page = Page::IMAGE_BOARD_THREAD_LIST; + body->clear_items(); + body->reset_selected(); + search_bar->clear(); + } + + BodyItem *selected_item = current_body->get_selected(); + if(event.key.code == sf::Keyboard::Enter && selected_item && (comment_navigation_stack.size() == 1 || current_body->selected_item != 0) && !selected_item->replies.empty()) { + // TODO: Optimize this by making body items shared_ptr instead of unique_ptr + Body *new_body = new Body(this, font); + new_body->draw_thumbnails = true; + new_body->items.push_back(std::make_unique(*selected_item)); + for(size_t reply_index : selected_item->replies) { + new_body->items.push_back(std::make_unique(*body->items[reply_index])); + } + comment_navigation_stack.push(new_body); + } else if(event.key.code == sf::Keyboard::BackSpace && comment_navigation_stack.size() > 1) { + delete comment_navigation_stack.top(); + comment_navigation_stack.pop(); + } + } + } + + // TODO: This code is duplicated in many places. Handle it in one place. + if(redraw) { + redraw = false; + 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_bottom); + } + + //search_bar->update(); + + window.clear(back_color); + comment_navigation_stack.top()->draw(window, body_pos, body_size); + //search_bar->draw(window); + window.display(); + } + + // We dont delete the first item since it's a reference to the global body, which we dont want to delete + // as it's also used for other pages. + // TODO: Remove the first item as well when each page has their own body + while(comment_navigation_stack.size() > 1) { + delete comment_navigation_stack.top(); + comment_navigation_stack.pop(); + } + } } \ No newline at end of file diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp index 01039b9..5a8cada 100644 --- a/src/plugins/Fourchan.cpp +++ b/src/plugins/Fourchan.cpp @@ -1,6 +1,7 @@ #include "../../plugins/Fourchan.hpp" #include #include +#include "../../include/DataView.hpp" #include #include @@ -348,11 +349,115 @@ namespace QuickMedia { } static bool string_ends_with(const std::string &str, const std::string &ends_with_str) { - size_t len = ends_with_str.size(); - return len == 0 || (str.size() >= len && memcmp(&str[str.size() - len], ends_with_str.data(), len) == 0); + size_t ends_len = ends_with_str.size(); + return ends_len == 0 || (str.size() >= ends_len && memcmp(&str[str.size() - ends_len], ends_with_str.data(), ends_len) == 0); } - PluginResult Fourchan::get_content_list(const std::string &url, BodyItems &result_items) { + struct CommentPiece { + enum class Type { + TEXT, + QUOTE, // > + QUOTELINK, // >>POSTNO, + LINEBREAK + }; + + DataView text; // Set when type is TEXT, QUOTE or QUOTELINK + int64_t quote_postnumber; // Set when type is QUOTELINK + Type type; + }; + + static TidyAttr get_attribute_by_name(TidyNode node, const char *name) { + for(TidyAttr attr = tidyAttrFirst(node); attr; attr = tidyAttrNext(attr)) { + const char *attr_name = tidyAttrName(attr); + if(attr_name && strcmp(name, attr_name) == 0) + return attr; + } + return nullptr; + } + + static const char* get_attribute_value(TidyNode node, const char *name) { + TidyAttr attr = get_attribute_by_name(node, name); + if(attr) + return tidyAttrValue(attr); + return nullptr; + } + + using CommentPieceCallback = std::function; + static void extract_comment_pieces(TidyDoc doc, TidyNode node, CommentPieceCallback callback) { + for(TidyNode child = tidyGetChild(node); child; child = tidyGetNext(child)) { + //extract_comment_pieces(doc, child, callback); + const char *node_name = tidyNodeGetName(child); + TidyNodeType node_type = tidyNodeGetType(child); + if(node_type == TidyNode_Start && node_name) { + TidyNode text_node = tidyGetChild(child); + //fprintf(stderr, "Child node name: %s, child text type: %d\n", node_name, tidyNodeGetType(text_node)); + if(tidyNodeGetType(text_node) == TidyNode_Text) { + TidyBuffer tidy_buffer; + tidyBufInit(&tidy_buffer); + if(tidyNodeGetText(doc, text_node, &tidy_buffer)) { + CommentPiece comment_piece; + comment_piece.type = CommentPiece::Type::TEXT; + comment_piece.text = { (char*)tidy_buffer.bp, tidy_buffer.size }; + if(strcmp(node_name, "span") == 0) { + const char *span_class = get_attribute_value(child, "class"); + //fprintf(stderr, "span class: %s\n", span_class); + if(span_class && strcmp(span_class, "quote") == 0) + comment_piece.type = CommentPiece::Type::QUOTE; + } else if(strcmp(node_name, "a") == 0) { + const char *a_class = get_attribute_value(child, "class"); + const char *a_href = get_attribute_value(child, "href"); + //fprintf(stderr, "a class: %s, href: %s\n", a_class, a_href); + if(a_class && a_href && strcmp(a_class, "quotelink") == 0 && strncmp(a_href, "#p", 2) == 0) { + comment_piece.type = CommentPiece::Type::QUOTELINK; + comment_piece.quote_postnumber = strtoll(a_href + 2, nullptr, 10); + } + } + /* + for(size_t i = 0; i < comment_piece.text.size; ++i) { + if(comment_piece.text.data[i] == '\n') + comment_piece.text.data[i] = ' '; + } + */ + callback(comment_piece); + } + tidyBufFree(&tidy_buffer); + } + } else if(node_type == TidyNode_Text) { + TidyBuffer tidy_buffer; + tidyBufInit(&tidy_buffer); + if(tidyNodeGetText(doc, child, &tidy_buffer)) { + CommentPiece comment_piece; + comment_piece.type = CommentPiece::Type::TEXT; + comment_piece.text = { (char*)tidy_buffer.bp, tidy_buffer.size }; + /* + for(size_t i = 0; i < comment_piece.text.size; ++i) { + if(comment_piece.text.data[i] == '\n') + comment_piece.text.data[i] = ' '; + } + */ + callback(comment_piece); + } + tidyBufFree(&tidy_buffer); + } + } + } + + static void extract_comment_pieces(const char *html_source, size_t size, CommentPieceCallback callback) { + TidyDoc doc = tidyCreate(); + tidyOptSetBool(doc, TidyShowWarnings, no); + if(tidyParseString(doc, html_source) < 0) { + CommentPiece comment_piece; + comment_piece.type = CommentPiece::Type::TEXT; + // Warning: Cast from const char* to char* ... + comment_piece.text = { (char*)html_source, size }; + callback(comment_piece); + } else { + extract_comment_pieces(doc, tidyGetBody(doc), std::move(callback)); + } + tidyRelease(doc); + } + + PluginResult Fourchan::get_threads(const std::string &url, BodyItems &result_items) { std::string server_response; if(download_to_string(fourchan_url + url + "/catalog.json", server_response) != DownloadResult::OK) return PluginResult::NET_ERR; @@ -380,14 +485,38 @@ namespace QuickMedia { continue; const Json::Value &com = thread["com"]; - if(!com.isString()) - continue; + const char *comment_begin = ""; + const char *comment_end = comment_begin; + com.getString(&comment_begin, &comment_end); const Json::Value &thread_num = thread["no"]; if(!thread_num.isNumeric()) continue; - auto body_item = std::make_unique(com.asString()); + std::string comment_text; + extract_comment_pieces(comment_begin, comment_end - comment_begin, + [&comment_text](const CommentPiece &cp) { + switch(cp.type) { + case CommentPiece::Type::TEXT: + comment_text.append(cp.text.data, cp.text.size); + break; + case CommentPiece::Type::QUOTE: + comment_text += '>'; + comment_text.append(cp.text.data, cp.text.size); + //comment_text += '\n'; + break; + case CommentPiece::Type::QUOTELINK: { + comment_text.append(cp.text.data, cp.text.size); + break; + } + case CommentPiece::Type::LINEBREAK: + // comment_text += '\n'; + break; + } + } + ); + html_unescape_sequences(comment_text); + auto body_item = std::make_unique(std::move(comment_text)); body_item->url = std::to_string(thread_num.asInt64()); const Json::Value &ext = thread["ext"]; @@ -411,95 +540,7 @@ namespace QuickMedia { return PluginResult::OK; } - struct CommentPiece { - enum class Type { - TEXT, - QUOTE, // > - QUOTELINK, // >>POSTNO, - LINEBREAK - }; - - std::string_view text; // Set when type is TEXT, QUOTE or QUOTELINK - int64_t quote_postnumber; // Set when type is QUOTELINK - Type type; - }; - - static TidyAttr get_attribute_by_name(TidyNode node, const char *name) { - for(TidyAttr attr = tidyAttrFirst(node); attr; attr = tidyAttrNext(attr)) { - const char *attr_name = tidyAttrName(attr); - if(attr_name && strcmp(name, attr_name) == 0) - return attr; - } - return nullptr; - } - - static const char* get_attribute_value(TidyNode node, const char *name) { - TidyAttr attr = get_attribute_by_name(node, name); - if(attr) - return tidyAttrValue(attr); - return nullptr; - } - - using CommentPieceCallback = std::function; - static void extract_comment_pieces(TidyDoc doc, TidyNode node, CommentPieceCallback callback) { - const char *node_name = tidyNodeGetName(node); - printf("node name: %s\n", node_name ? node_name : "N/A"); - - TidyNodeType node_type = tidyNodeGetType(node); - if(node_type == TidyNode_Text) { - TidyBuffer tidy_buffer; - tidyBufInit(&tidy_buffer); - if(tidyNodeGetText(doc, node, &tidy_buffer)) { - CommentPiece comment_piece; - comment_piece.type = CommentPiece::Type::TEXT; - if(node_name) { - if(strncmp("a", (const char*)tidy_buffer.bp, tidy_buffer.size) == 0) { - const char *a_href = get_attribute_value(node, "href"); - if(a_href && strncmp(a_href, "#p", 2) == 0) { - comment_piece.type = CommentPiece::Type::QUOTELINK; - comment_piece.quote_postnumber = strtoll(a_href + 2, nullptr, 10); - } - } else if(strncmp("span", (const char*)tidy_buffer.bp, tidy_buffer.size) == 0) { - const char *a_class = get_attribute_value(node, "class"); - if(a_class && strcmp(a_class, "quote") == 0) { - comment_piece.type = CommentPiece::Type::QUOTE; - comment_piece.text = { (const char*)tidy_buffer.bp, tidy_buffer.size }; - } - } - } - printf("Comment piece value: %.*s\n", tidy_buffer.size, tidy_buffer.bp); - comment_piece.text = { (const char*)tidy_buffer.bp, tidy_buffer.size }; - callback(comment_piece); - } - tidyBufFree(&tidy_buffer); - } else if(node_name) { - if(strcmp("br", node_name) == 0) { - CommentPiece comment_piece; - comment_piece.type = CommentPiece::Type::LINEBREAK; - callback(comment_piece); - } - } - - for(TidyNode child = tidyGetChild(node); child; child = tidyGetNext(child)) { - extract_comment_pieces(doc, child, callback); - } - } - - static void extract_comment_pieces(const char *html_source, size_t size, CommentPieceCallback callback) { - TidyDoc doc = tidyCreate(); - tidyOptSetBool(doc, TidyShowWarnings, no); - if(tidyParseString(doc, html_source) < 0) { - CommentPiece comment_piece; - comment_piece.type = CommentPiece::Type::TEXT; - comment_piece.text = { html_source, size }; - callback(comment_piece); - } else { - extract_comment_pieces(doc, tidyGetBody(doc), std::move(callback)); - } - tidyRelease(doc); - } - - PluginResult Fourchan::get_content_details(const std::string &list_url, const std::string &url, BodyItems &result_items) { + PluginResult Fourchan::get_thread_comments(const std::string &list_url, const std::string &url, BodyItems &result_items) { std::string server_response; if(download_to_string(fourchan_url + list_url + "/thread/" + url + ".json", server_response) != DownloadResult::OK) return PluginResult::NET_ERR; @@ -513,43 +554,70 @@ namespace QuickMedia { return PluginResult::ERR; } + std::unordered_map comment_by_postno; + const Json::Value &posts = json_root["posts"]; if(posts.isArray()) { for(const Json::Value &post : posts) { if(!post.isObject()) continue; - const Json::Value &com = post["com"]; - const char *comment_begin; - const char *comment_end; - if(!com.getString(&comment_begin, &comment_end)) + const Json::Value &post_num = post["no"]; + if(!post_num.isNumeric()) continue; + + comment_by_postno[post_num.asInt64()] = result_items.size(); + result_items.push_back(std::make_unique("")); + } + } + + size_t body_item_index = 0; + if(posts.isArray()) { + for(const Json::Value &post : posts) { + if(!post.isObject()) + continue; + + const Json::Value &com = post["com"]; + const char *comment_begin = ""; + const char *comment_end = comment_begin; + com.getString(&comment_begin, &comment_end); const Json::Value &post_num = post["no"]; if(!post_num.isNumeric()) continue; std::string comment_text; - extract_comment_pieces(comment_begin, comment_end - comment_begin, [&comment_text](const CommentPiece &cp) { - switch(cp.type) { - case CommentPiece::Type::TEXT: - comment_text += cp.text; - break; - case CommentPiece::Type::QUOTE: - comment_text += '>'; - comment_text += cp.text; - break; - case CommentPiece::Type::QUOTELINK: - comment_text += cp.text; - break; - case CommentPiece::Type::LINEBREAK: - // comment_text += '\n'; - break; + extract_comment_pieces(comment_begin, comment_end - comment_begin, + [&comment_text, &comment_by_postno, &result_items, body_item_index](const CommentPiece &cp) { + switch(cp.type) { + case CommentPiece::Type::TEXT: + comment_text.append(cp.text.data, cp.text.size); + break; + case CommentPiece::Type::QUOTE: + comment_text += '>'; + comment_text.append(cp.text.data, cp.text.size); + //comment_text += '\n'; + break; + case CommentPiece::Type::QUOTELINK: { + comment_text.append(cp.text.data, cp.text.size); + auto it = comment_by_postno.find(cp.quote_postnumber); + if(it == comment_by_postno.end()) { + // TODO: Link this quote to a 4chan archive that still has the quoted comment (if available) + comment_text += "(dead)"; + } else { + result_items[it->second]->replies.push_back(body_item_index); + } + break; + } + case CommentPiece::Type::LINEBREAK: + // comment_text += '\n'; + break; + } } - }); + ); html_unescape_sequences(comment_text); - auto body_item = std::make_unique(std::move(comment_text)); - body_item->url = std::to_string(post_num.asInt64()); + BodyItem *body_item = result_items[body_item_index].get(); + body_item->title = std::move(comment_text); const Json::Value &ext = post["ext"]; const Json::Value &tim = post["tim"]; @@ -564,7 +632,7 @@ namespace QuickMedia { body_item->thumbnail_url = fourchan_image_url + list_url + "/" + std::to_string(tim.asInt64()) + "s.jpg"; } - result_items.emplace_back(std::move(body_item)); + ++body_item_index; } } -- cgit v1.2.3