aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--images/4chan_logo.pngbin0 -> 7852 bytes
-rw-r--r--include/Page.hpp4
-rw-r--r--include/QuickMedia.hpp6
-rw-r--r--plugins/Fourchan.hpp18
-rw-r--r--plugins/Plugin.hpp22
-rw-r--r--src/Body.cpp4
-rw-r--r--src/QuickMedia.cpp212
-rw-r--r--src/plugins/Fourchan.cpp187
-rw-r--r--src/plugins/Plugin.cpp34
9 files changed, 457 insertions, 30 deletions
diff --git a/images/4chan_logo.png b/images/4chan_logo.png
new file mode 100644
index 0000000..2baed50
--- /dev/null
+++ b/images/4chan_logo.png
Binary files differ
diff --git a/include/Page.hpp b/include/Page.hpp
index 7b1fc17..b159877 100644
--- a/include/Page.hpp
+++ b/include/Page.hpp
@@ -7,6 +7,8 @@ namespace QuickMedia {
SEARCH_RESULT,
VIDEO_CONTENT,
EPISODE_LIST,
- IMAGES
+ IMAGES,
+ CONTENT_LIST,
+ CONTENT_DETAILS
};
} \ No newline at end of file
diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp
index 5010025..70c7001 100644
--- a/include/QuickMedia.hpp
+++ b/include/QuickMedia.hpp
@@ -27,6 +27,10 @@ namespace QuickMedia {
void video_content_page();
void episode_list_page();
void image_page();
+ void content_list_page();
+ void content_details_page();
+
+ void select_episode(BodyItem *item, bool start_from_beginning);
private:
sf::RenderWindow window;
sf::Vector2f window_size;
@@ -39,7 +43,9 @@ namespace QuickMedia {
// TODO: Combine these
std::string images_url;
std::string content_title;
+ std::string content_episode;
std::string content_url;
+ std::string content_list_url;
std::string chapter_title;
int image_index;
Path content_storage_file;
diff --git a/plugins/Fourchan.hpp b/plugins/Fourchan.hpp
new file mode 100644
index 0000000..d6b482b
--- /dev/null
+++ b/plugins/Fourchan.hpp
@@ -0,0 +1,18 @@
+#pragma once
+
+#include "Plugin.hpp"
+
+namespace QuickMedia {
+ class Fourchan : public Plugin {
+ public:
+ 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;
+ 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; }
+ };
+} \ No newline at end of file
diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp
index a999d36..d5802af 100644
--- a/plugins/Plugin.hpp
+++ b/plugins/Plugin.hpp
@@ -7,6 +7,12 @@
#include <memory>
namespace QuickMedia {
+ enum class PluginResult {
+ OK,
+ ERR,
+ NET_ERR
+ };
+
enum class SearchResult {
OK,
ERR,
@@ -39,14 +45,30 @@ namespace QuickMedia {
DownloadResult download_to_string(const std::string &url, std::string &result, const std::vector<CommandArg> &additional_args = {});
std::string strip(const std::string &str);
+ void string_replace_all(std::string &str, const std::string &old_str, const std::string &new_str);
+ void html_unescape_sequences(std::string &str);
class Plugin {
public:
virtual ~Plugin() = default;
+ virtual PluginResult get_front_page(BodyItems &result_items) {
+ (void)result_items; return PluginResult::OK;
+ }
virtual SearchResult search(const std::string &text, BodyItems &result_items);
virtual SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items);
virtual BodyItems get_related_media(const std::string &url);
+ virtual PluginResult get_content_list(const std::string &url, BodyItems &result_items) {
+ (void)url;
+ (void)result_items;
+ return PluginResult::OK;
+ }
+ virtual PluginResult get_content_details(const std::string &list_url, const std::string &url, BodyItems &result_items) {
+ (void)list_url;
+ (void)url;
+ (void)result_items;
+ return PluginResult::OK;
+ }
virtual bool search_suggestions_has_thumbnails() const = 0;
virtual bool search_results_has_thumbnails() const = 0;
virtual int get_search_delay() const = 0;
diff --git a/src/Body.cpp b/src/Body.cpp
index c87844c..fb44929 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -54,6 +54,7 @@ namespace QuickMedia {
void Body::clear_items() {
items.clear();
+ selected_item = 0;
//item_thumbnail_textures.clear();
}
@@ -151,6 +152,7 @@ namespace QuickMedia {
// Find the starting row that can be drawn to make selected row visible as well
int visible_rows = 0;
int first_visible_item = selected_item;
+ assert(first_visible_item >= 0 && first_visible_item < (int)items.size());
for(; first_visible_item >= 0 && visible_rows < max_visible_rows; --first_visible_item) {
auto &item = items[first_visible_item];
if(item->visible)
@@ -235,7 +237,7 @@ namespace QuickMedia {
const Json::Value &current_json = item_progress["current"];
const Json::Value &total_json = item_progress["total"];
if(current_json.isNumeric() && total_json.isNumeric()) {
- progress_text.setString(std::string("Progress: ") + std::to_string(current_json.asInt()) + "/" + std::to_string(total_json.asInt()));
+ progress_text.setString(std::string("Page: ") + std::to_string(current_json.asInt()) + "/" + std::to_string(total_json.asInt()));
auto bounds = progress_text.getLocalBounds();
progress_text.setPosition(std::floor(item_pos.x + size.x - bounds.width - 10.0f), std::floor(item_pos.y));
window.draw(progress_text);
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 73e44b9..05c9004 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -2,6 +2,7 @@
#include "../plugins/Manganelo.hpp"
#include "../plugins/Youtube.hpp"
#include "../plugins/Pornhub.hpp"
+#include "../plugins/Fourchan.hpp"
#include "../include/Scale.hpp"
#include "../include/Program.h"
#include "../include/VideoPlayer.hpp"
@@ -103,6 +104,9 @@ namespace QuickMedia {
} else if(strcmp(argv[1], "pornhub") == 0) {
current_plugin = new Pornhub();
plugin_logo_path = "../../../images/pornhub_logo.png";
+ } else if(strcmp(argv[1], "4chan") == 0) {
+ current_plugin = new Fourchan();
+ plugin_logo_path = "../../../images/4chan_logo.png";
} else {
usage();
return -1;
@@ -150,6 +154,16 @@ namespace QuickMedia {
window.setKeyRepeatEnabled(true);
break;
}
+ case Page::CONTENT_LIST: {
+ body->draw_thumbnails = true;
+ content_list_page();
+ break;
+ }
+ case Page::CONTENT_DETAILS: {
+ body->draw_thumbnails = true;
+ content_details_page();
+ break;
+ }
default:
window.close();
break;
@@ -233,7 +247,7 @@ namespace QuickMedia {
return file_overwrite(path, Json::writeString(json_builder, json)) == 0;
}
- static std::string manga_extract_id_from_url(const std::string &url) {
+ static bool manga_extract_id_from_url(const std::string &url, std::string &manga_id) {
bool manganelo_website = false;
if(url.find("mangakakalot") != std::string::npos)
manganelo_website = true;
@@ -247,24 +261,24 @@ namespace QuickMedia {
err_msg += url;
err_msg += " doesn't contain manga id";
show_notification("Manga", err_msg, Urgency::CRITICAL);
- abort();
+ return false;
}
- std::string manga_id = url.substr(index + 6);
+ manga_id = url.substr(index + 6);
if(manga_id.size() <= 2) {
std::string err_msg = "Url ";
err_msg += url;
err_msg += " doesn't contain manga id";
show_notification("Manga", err_msg, Urgency::CRITICAL);
- abort();
+ return false;
}
- return manga_id;
+ return true;
} else {
std::string err_msg = "Unexpected url ";
err_msg += url;
err_msg += " is not manganelo or mangakakalot";
show_notification("Manga", err_msg, Urgency::CRITICAL);
- abort();
+ return false;
}
}
@@ -292,7 +306,9 @@ namespace QuickMedia {
return false;
}
- std::string manga_id = manga_extract_id_from_url(content_url);
+ std::string manga_id;
+ if(!manga_extract_id_from_url(content_url, manga_id))
+ return false;
content_storage_file = content_storage_dir.join(base64_encode(manga_id));
content_storage_json.clear();
content_storage_json["name"] = content_title;
@@ -303,11 +319,16 @@ namespace QuickMedia {
watched_videos.clear();
if(content_url.empty())
next_page = Page::SEARCH_RESULT;
+ } else if(next_page == Page::CONTENT_LIST) {
+ content_list_url = content_url;
}
current_page = next_page;
return true;
};
+ PluginResult front_page_result = current_plugin->get_front_page(body->items);
+ body->clamp_selection();
+
sf::Vector2f body_pos;
sf::Vector2f body_size;
bool resized = true;
@@ -370,7 +391,7 @@ namespace QuickMedia {
#if 0
search_bar->onTextUpdateCallback = [this](const std::string &text) {
body->filter_search_fuzzy(text);
- body->clamp_selection();
+ body->selected_item = 0;
};
search_bar->onTextSubmitCallback = [this](const std::string &text) {
@@ -649,7 +670,7 @@ namespace QuickMedia {
if(sf::Mouse::isButtonPressed(sf::Mouse::Left)) {
auto mouse_pos = sf::Mouse::getPosition(window);
- if(mouse_pos.y >= window_size.y - ui_height) {
+ if(mouse_pos.y >= window_size.y - ui_height && mouse_pos.y <= window_size.y) {
if(seekable)
video_player->set_progress((double)mouse_pos.x / (double)window_size.x);
else
@@ -687,10 +708,29 @@ namespace QuickMedia {
return exec_program(args, nullptr, nullptr);
}
+ void Program::select_episode(BodyItem *item, bool start_from_beginning) {
+ images_url = item->url;
+ chapter_title = item->title;
+ image_index = 0;
+ current_page = Page::IMAGES;
+ if(start_from_beginning)
+ return;
+
+ const Json::Value &json_chapters = content_storage_json["chapters"];
+ if(json_chapters.isObject()) {
+ const Json::Value &json_chapter = json_chapters[chapter_title];
+ if(json_chapter.isObject()) {
+ const Json::Value &current = json_chapter["current"];
+ if(current.isNumeric())
+ image_index = current.asInt() - 1;
+ }
+ }
+ }
+
void Program::episode_list_page() {
search_bar->onTextUpdateCallback = [this](const std::string &text) {
body->filter_search_fuzzy(text);
- body->clamp_selection();
+ body->selected_item = 0;
};
search_bar->onTextSubmitCallback = [this](const std::string &text) -> bool {
@@ -698,21 +738,7 @@ namespace QuickMedia {
if(!selected_item)
return false;
- images_url = selected_item->url;
- chapter_title = selected_item->title;
- image_index = 0;
- current_page = Page::IMAGES;
-
- const Json::Value &json_chapters = content_storage_json["chapters"];
- if(json_chapters.isObject()) {
- const Json::Value &json_chapter = json_chapters[chapter_title];
- if(json_chapter.isObject()) {
- const Json::Value &current = json_chapter["current"];
- if(current.isNumeric())
- image_index = current.asInt() - 1;
- }
- }
-
+ select_episode(selected_item, false);
return true;
};
@@ -790,8 +816,7 @@ namespace QuickMedia {
error_message.setString(std::string("Failed to load image for page ") + std::to_string(image_index + 1));
}
} else if(image_result == ImageResult::END) {
- // TODO: Improve this message
- error_message.setString("End of chapter");
+ error_message.setString("End of " + chapter_title);
} 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 + 1));
@@ -800,6 +825,7 @@ namespace QuickMedia {
int num_images = 0;
image_plugin->get_number_of_images(images_url, num_images);
+ image_index = std::min(image_index, num_images);
Json::Value &json_chapters = content_storage_json["chapters"];
Json::Value json_chapter;
@@ -858,11 +884,22 @@ namespace QuickMedia {
if(image_index > 0) {
--image_index;
return;
+ } else if(image_index == 0 && body->selected_item < (int)body->items.size() - 1) {
+ // TODO: Make this work if the list is sorted differently than from newest to oldest.
+ body->selected_item++;
+ select_episode(body->items[body->selected_item].get(), true);
+ image_index = 99999; // Start at the page that shows we are at the end of the chapter
+ return;
}
} else if(event.key.code == sf::Keyboard::Down) {
if(image_index < num_images) {
++image_index;
return;
+ } else if(image_index == num_images && body->selected_item > 0) {
+ // TODO: Make this work if the list is sorted differently than from newest to oldest.
+ body->selected_item--;
+ select_episode(body->items[body->selected_item].get(), true);
+ return;
}
} else if(event.key.code == sf::Keyboard::Escape) {
current_page = Page::EPISODE_LIST;
@@ -913,4 +950,125 @@ namespace QuickMedia {
window.display();
}
}
+
+ void Program::content_list_page() {
+ if(current_plugin->get_content_list(content_list_url, body->items) != PluginResult::OK) {
+ show_notification("Content list", "Failed to get content list for url: " + content_list_url, Urgency::CRITICAL);
+ current_page = Page::SEARCH_SUGGESTION;
+ return;
+ }
+
+ search_bar->onTextUpdateCallback = [this](const std::string &text) {
+ body->filter_search_fuzzy(text);
+ body->selected_item = 0;
+ };
+
+ 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::CONTENT_DETAILS;
+ body->clear_items();
+ return true;
+ };
+
+ sf::Vector2f body_pos;
+ sf::Vector2f body_size;
+ bool resized = true;
+ sf::Event event;
+
+ while (current_page == Page::CONTENT_LIST) {
+ while (window.pollEvent(event)) {
+ base_event_handler(event, Page::SEARCH_SUGGESTION);
+ if(event.type == sf::Event::Resized)
+ resized = true;
+ }
+
+ // TODO: This code is duplicated in many places. Handle it in one place.
+ 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_bottom);
+ }
+
+ search_bar->update();
+
+ window.clear(back_color);
+ body->draw(window, body_pos, body_size);
+ search_bar->draw(window);
+ window.display();
+ }
+ }
+
+ void Program::content_details_page() {
+ if(current_plugin->get_content_details(content_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::CONTENT_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 resized = true;
+ sf::Event event;
+
+ while (current_page == Page::CONTENT_DETAILS) {
+ while (window.pollEvent(event)) {
+ base_event_handler(event, Page::SEARCH_SUGGESTION);
+ if(event.type == sf::Event::Resized)
+ resized = true;
+ }
+
+ // TODO: This code is duplicated in many places. Handle it in one place.
+ 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_bottom);
+ }
+
+ search_bar->update();
+
+ window.clear(back_color);
+ body->draw(window, body_pos, body_size);
+ search_bar->draw(window);
+ window.display();
+ }
+ }
} \ No newline at end of file
diff --git a/src/plugins/Fourchan.cpp b/src/plugins/Fourchan.cpp
new file mode 100644
index 0000000..0c3bffb
--- /dev/null
+++ b/src/plugins/Fourchan.cpp
@@ -0,0 +1,187 @@
+#include "../../plugins/Fourchan.hpp"
+#include <json/reader.h>
+#include <string.h>
+
+// API documentation: https://github.com/4chan/4chan-API
+
+static const std::string fourchan_url = "https://a.4cdn.org/";
+static const std::string fourchan_image_url = "https://i.4cdn.org/";
+
+// Legacy recaptcha command: curl 'https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc' -H 'Referer: https://boards.4channel.org/' -H 'Cookie: CONSENT=YES'
+
+/*
+Answering recaptcha:
+curl 'https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc'
+-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0'
+-H 'Referer: https://www.google.com/recaptcha/api/fallback?k=6Ldp2bsSAAAAAAJ5uyx_lx34lJeEpTLVkP5k04qc'
+-H 'Content-Type: application/x-www-form-urlencoded'
+--data 'c=03AOLTBLQ66PjSi9s8S-R1vUS2Jgm-Z_ghEejvvjAaeF3FoR9MiM0zHhCxuertrCo7MAcFUEqcIg4l2WJzVtrJhJVLkncF12OzCaeIvbm46hgDZDZjLD89-LMn1Zs0TP37P-Hd4cuRG8nHuEBXc2ZBD8CVX-6HAs9VBgSmsgQeKF1PWm1tAMBccJhlh4rAOkpjzaEXMMGOe17N0XViwDYZxLGhe4H8IAG2KNB1fb4rz4YKJTPbL30_FvHw7zkdFtojjWiqVW0yCN6N192dhfd9oKz2r9pGRrR6N4AkkX-L0DsBD4yNK3QRsQn3dB1fs3JRZPAh1yqUqTQYhOaqdggyc1EwL8FZHouGRkHTOcCmLQjyv6zuhi6CJbg&response=1&response=4&response=5&response=7'
+*/
+
+/* Posting message:
+curl 'https://sys.4chan.org/bant/post'
+-H 'User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:68.0) Gecko/20100101 Firefox/68.0'
+-H 'Referer: https://boards.4chan.org/'
+-H 'Content-Type: multipart/form-data; boundary=---------------------------119561554312148213571335532670'
+-H 'Origin: https://boards.4chan.org'
+-H 'Cookie: __cfduid=d4bd4932e46bc3272fae4ce7a4e2aac511546800687; 4chan_pass=_SsBuZaATt3dIqfVEWlpemhU5XLQ6i9RC'
+--data-binary $'-----------------------------119561554312148213571335532670\r\nContent-Disposition: form-data; name="resto"\r\n\r\n8640736\r\n-----------------------------119561554312148213571335532670\r\nContent-Disposition: form-data; name="com"\r\n\r\n>>8640771\r\nShe looks finnish\r\n-----------------------------119561554312148213571335532670\r\nContent-Disposition: form-data; name="mode"\r\n\r\nregist\r\n-----------------------------119561554312148213571335532670\r\nContent-Disposition: form-data; name="pwd"\r\n\r\n_SsBuZaATt3dIqfVEWlpemhU5XLQ6i9RC\r\n-----------------------------119561554312148213571335532670\r\nContent-Disposition: form-data; name="g-recaptcha-response"\r\n\r\n03AOLTBLS5lshp5aPj5pG6xdVMQ0pHuHxAtJoCEYuPLNKYlsRWNCPQegjB9zgL-vwdGMzjcT-L9iW4bnQ5W3TqUWHOVqtsfnx9GipLUL9o2XbC6r9zy-EEiPde7l6J0WcZbr9nh_MGcUpKl6RGaZoYB3WwXaDq74N5hkmEAbqM_CBtbAVVlQyPmemI2HhO2J6K0yFVKBrBingtIZ6-oXBXZ4jC4rT0PeOuVaH_gf_EBjTpb55ueaPmTbeLGkBxD4-wL1qA8F8h0D8c\r\n-----------------------------119561554312148213571335532670--\r\n'
+*/
+
+namespace QuickMedia {
+ PluginResult Fourchan::get_front_page(BodyItems &result_items) {
+ std::string server_response;
+ if(download_to_string(fourchan_url + "boards.json", server_response) != DownloadResult::OK)
+ return PluginResult::NET_ERR;
+
+ Json::Value json_root;
+ Json::CharReaderBuilder json_builder;
+ std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader());
+ std::string json_errors;
+ if(!json_reader->parse(server_response.data(), server_response.data() + server_response.size(), &json_root, &json_errors)) {
+ fprintf(stderr, "4chan front page json error: %s\n", json_errors.c_str());
+ return PluginResult::ERR;
+ }
+
+ const Json::Value &boards = json_root["boards"];
+ if(boards.isArray()) {
+ for(const Json::Value &board : boards) {
+ const Json::Value &board_id = board["board"]; // /g/, /a/, /b/ etc
+ const Json::Value &board_title = board["title"];
+ const Json::Value &board_description = board["meta_description"];
+ if(board_id.isString() && board_title.isString() && board_description.isString()) {
+ std::string board_description_str = board_description.asString();
+ html_unescape_sequences(board_description_str);
+ auto body_item = std::make_unique<BodyItem>(board_title.asString());
+ body_item->url = board_id.asString();
+ result_items.emplace_back(std::move(body_item));
+ }
+ }
+ }
+
+ return PluginResult::OK;
+ }
+
+ SearchResult Fourchan::search(const std::string &url, BodyItems &result_items) {
+ return SearchResult::OK;
+ }
+
+ SuggestionResult Fourchan::update_search_suggestions(const std::string &text, BodyItems &result_items) {
+ return SuggestionResult::OK;
+ }
+
+ 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);
+ }
+
+ PluginResult Fourchan::get_content_list(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;
+
+ Json::Value json_root;
+ Json::CharReaderBuilder json_builder;
+ std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader());
+ std::string json_errors;
+ if(!json_reader->parse(server_response.data(), server_response.data() + server_response.size(), &json_root, &json_errors)) {
+ fprintf(stderr, "4chan catalog json error: %s\n", json_errors.c_str());
+ return PluginResult::ERR;
+ }
+
+ if(json_root.isArray()) {
+ for(const Json::Value &page_data : json_root) {
+ if(!page_data.isObject())
+ continue;
+
+ const Json::Value &threads = page_data["threads"];
+ if(!threads.isArray())
+ continue;
+
+ for(const Json::Value &thread : threads) {
+ if(!thread.isObject())
+ continue;
+
+ const Json::Value &com = thread["com"];
+ if(!com.isString())
+ continue;
+
+ const Json::Value &thread_num = thread["no"];
+ if(!thread_num.isNumeric())
+ continue;
+
+ auto body_item = std::make_unique<BodyItem>(com.asString());
+ body_item->url = std::to_string(thread_num.asInt64());
+
+ const Json::Value &ext = thread["ext"];
+ const Json::Value &tim = thread["tim"];
+ if(tim.isNumeric() && ext.isString()) {
+ std::string ext_str = ext.asString();
+ if(ext_str == ".png" || ext_str == ".jpg" || ext_str == ".jpeg") {
+ } else {
+ fprintf(stderr, "TODO: Support file extension: %s\n", ext_str.c_str());
+ }
+ // "s" means small, that's the url 4chan uses for thumbnails.
+ // thumbnails always has .jpg extension even if they are gifs or webm.
+ body_item->thumbnail_url = fourchan_image_url + url + "/" + std::to_string(tim.asInt64()) + "s.jpg";
+ }
+
+ result_items.emplace_back(std::move(body_item));
+ }
+ }
+ }
+
+ return PluginResult::OK;
+ }
+
+ PluginResult Fourchan::get_content_details(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;
+
+ Json::Value json_root;
+ Json::CharReaderBuilder json_builder;
+ std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader());
+ std::string json_errors;
+ if(!json_reader->parse(server_response.data(), server_response.data() + server_response.size(), &json_root, &json_errors)) {
+ fprintf(stderr, "4chan thread json error: %s\n", json_errors.c_str());
+ return PluginResult::ERR;
+ }
+
+ 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"];
+ if(!com.isString())
+ continue;
+
+ const Json::Value &post_num = post["no"];
+ if(!post_num.isNumeric())
+ continue;
+
+ auto body_item = std::make_unique<BodyItem>(com.asString());
+ body_item->url = std::to_string(post_num.asInt64());
+
+ const Json::Value &ext = post["ext"];
+ const Json::Value &tim = post["tim"];
+ if(tim.isNumeric() && ext.isString()) {
+ std::string ext_str = ext.asString();
+ if(ext_str == ".png" || ext_str == ".jpg" || ext_str == ".jpeg") {
+ } else {
+ fprintf(stderr, "TODO: Support file extension: %s\n", ext_str.c_str());
+ }
+ // "s" means small, that's the url 4chan uses for thumbnails.
+ // thumbnails always has .jpg extension even if they are gifs or webm.
+ body_item->thumbnail_url = fourchan_image_url + list_url + "/" + std::to_string(tim.asInt64()) + "s.jpg";
+ }
+
+ result_items.emplace_back(std::move(body_item));
+ }
+ }
+
+ return PluginResult::OK;
+ }
+} \ No newline at end of file
diff --git a/src/plugins/Plugin.cpp b/src/plugins/Plugin.cpp
index 79c6403..c56ff67 100644
--- a/src/plugins/Plugin.cpp
+++ b/src/plugins/Plugin.cpp
@@ -2,6 +2,7 @@
#include "../../include/Program.h"
#include <sstream>
#include <iomanip>
+#include <array>
static int accumulate_string(char *data, int size, void *userdata) {
std::string *str = (std::string*)userdata;
@@ -28,7 +29,8 @@ namespace QuickMedia {
}
DownloadResult download_to_string(const std::string &url, std::string &result, const std::vector<CommandArg> &additional_args) {
- std::vector<const char*> args = { "curl", "-H", "Accept-Language: en-US,en;q=0.5", "--compressed", "-s", "-L" };
+ sf::Clock timer;
+ std::vector<const char*> args = { "curl", "-f", "-H", "Accept-Language: en-US,en;q=0.5", "--compressed", "-s", "-L" };
for(const CommandArg &arg : additional_args) {
args.push_back(arg.option.c_str());
args.push_back(arg.value.c_str());
@@ -38,6 +40,7 @@ namespace QuickMedia {
args.push_back(nullptr);
if(exec_program(args.data(), accumulate_string, &result) != 0)
return DownloadResult::NET_ERR;
+ fprintf(stderr, "Download duration for %s: %d ms\n", url.c_str(), timer.getElapsedTime().asMilliseconds());
return DownloadResult::OK;
}
@@ -64,6 +67,35 @@ namespace QuickMedia {
return str.substr(start, end - start + 1);
}
+ struct HtmlEscapeSequence {
+ std::string escape_sequence;
+ std::string unescaped_str;
+ };
+
+ void string_replace_all(std::string &str, const std::string &old_str, const std::string &new_str) {
+ size_t index = 0;
+ while(true) {
+ index = str.find(old_str, index);
+ if(index == std::string::npos)
+ return;
+ str.replace(index, old_str.size(), new_str);
+ }
+ }
+
+ void html_unescape_sequences(std::string &str) {
+ const std::array<HtmlEscapeSequence, 5> escape_sequences = {
+ HtmlEscapeSequence { "&quot;", "\"" },
+ HtmlEscapeSequence { "&#39;", "'" },
+ HtmlEscapeSequence { "&lt;", "<" },
+ HtmlEscapeSequence { "&gt;", ">" },
+ HtmlEscapeSequence { "&amp;", "&" } // This should be last, to not accidentally replace a new sequence caused by replacing this
+ };
+
+ for(const HtmlEscapeSequence &escape_sequence : escape_sequences) {
+ string_replace_all(str, escape_sequence.escape_sequence, escape_sequence.unescaped_str);
+ }
+ }
+
std::string Plugin::url_param_encode(const std::string &param) const {
std::ostringstream result;
result.fill('0');