aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authordec05eba <dec05eba@protonmail.com>2021-08-16 21:13:24 +0200
committerdec05eba <dec05eba@protonmail.com>2021-08-16 21:13:24 +0200
commit5cc735b22570f1667d62958e59ce4910b529f5af (patch)
tree75128a8926a48a612bc892d266032bd7afd9c2cf /src
parentde4825e548b990493b372237cbef9a790bf114c4 (diff)
Add MyAnimeList (wip)
Diffstat (limited to 'src')
-rw-r--r--src/Body.cpp6
-rw-r--r--src/NetUtils.cpp16
-rw-r--r--src/QuickMedia.cpp10
-rw-r--r--src/StringUtils.cpp20
-rw-r--r--src/plugins/MangaGeneric.cpp2
-rw-r--r--src/plugins/Mangadex.cpp1
-rw-r--r--src/plugins/Manganelo.cpp2
-rw-r--r--src/plugins/Matrix.cpp13
-rw-r--r--src/plugins/MediaGeneric.cpp10
-rw-r--r--src/plugins/MyAnimeList.cpp311
10 files changed, 362 insertions, 29 deletions
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<HtmlUnescapeSequence, 4> unescape_sequences = {
+ // TODO: Use string find and find & and ; instead of string_replace_all
+ const std::array<HtmlUnescapeSequence, 6> unescape_sequences = {
HtmlUnescapeSequence { "&quot;", "\"" },
HtmlUnescapeSequence { "&lt;", "<" },
HtmlUnescapeSequence { "&gt;", ">" },
+ HtmlUnescapeSequence { "&mdash;", "—" },
+ HtmlUnescapeSequence { "&nbsp;", " " },
HtmlUnescapeSequence { "&amp;", "&" } // 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<const char*, const char*> 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 <directory>] [-e <window>] [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<HotExamplesLanguageSelectPage>(this), create_search_bar("Search...", SEARCH_DELAY_FILTER)});
+ } else if(strcmp(plugin_name, "mal") == 0) {
+ tabs.push_back(Tab{create_body(), std::make_unique<MyAnimeListSearchPage>(this), create_search_bar("Search...", 400)});
} else if(strcmp(plugin_name, "file-manager") == 0) {
auto file_manager_page = std::make_unique<FileManagerPage>(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<MediaTextQuery> &text_queries, const std::vector<MediaThumbnailQuery> &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<MediaTextQuery> &text_queries, const std::vector<MediaThumbnailQuery> &thumbnail_queries, sf::Vector2i thumbnail_max_size, MediaRelatedCustomHandler *custom_handler, BodyItems &result_items, bool cloudflare_bypass) {
std::vector<CommandArg> 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<Tab> &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 <quickmedia/HtmlSearch.h>
+
+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<BodyItem> 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<Tab> &result_tabs) {
+ if(url.empty())
+ return PluginResult::OK;
+
+ result_tabs.push_back({ create_body(), std::make_unique<MyAnimeListDetailsPage>(program, url), nullptr });
+ result_tabs.push_back({ create_body(false, true), std::make_unique<MyAnimeListRecommendationsPage>(program, url), nullptr });
+ return PluginResult::OK;
+ }
+
+ PluginResult MyAnimeListDetailsPage::submit(const std::string &title, const std::string &url, std::vector<Tab> &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<Tab> &result_tabs) {
+ result_tabs.push_back({ create_body(), std::make_unique<MyAnimeListDetailsPage>(program, url), nullptr });
+ result_tabs.push_back({ create_body(false, true), std::make_unique<MyAnimeListRecommendationsPage>(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