aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--TODO4
m---------depends/html-search0
-rw-r--r--images/mal_logo.pngbin0 -> 5858 bytes
-rw-r--r--include/StringUtils.hpp1
-rw-r--r--plugins/HotExamples.hpp4
-rw-r--r--plugins/Info.hpp2
-rw-r--r--plugins/MangaCombined.hpp1
-rw-r--r--plugins/MangaGeneric.hpp2
-rw-r--r--plugins/Mangadex.hpp1
-rw-r--r--plugins/Manganelo.hpp2
-rw-r--r--plugins/Matrix.hpp8
-rw-r--r--plugins/MediaGeneric.hpp1
-rw-r--r--plugins/MyAnimeList.hpp36
-rw-r--r--plugins/NyaaSi.hpp4
-rw-r--r--plugins/Page.hpp4
-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
26 files changed, 414 insertions, 51 deletions
diff --git a/README.md b/README.md
index 04bb32f..f7147d5 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
# QuickMedia
A dmenu-inspired native client for web services.
-Currently supported web services: `youtube`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao`, `hotexamples` and _others_.\
+Currently supported web services: `youtube`, `soundcloud`, `nyaa.si`, `manganelo`, `manganelos`, `mangatown`, `mangakatana`, `mangadex`, `readm`, `onimanga`, `4chan`, `matrix`, `saucenao`, `hotexamples`, `mal` and _others_.\
Config data, including manga progress is stored under `$HOME/.config/quickmedia`.\
Cache is stored under `$HOME/.cache/quickmedia`.
## Usage
```
usage: quickmedia [plugin] [--use-system-mpv-config] [--dir <directory>] [-e <window>] [youtube-url]
OPTIONS:
- plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, file-manager or stdin
+ plugin The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, readm, onimanga, youtube, soundcloud, nyaa.si, matrix, saucenao, hotexamples, mal, file-manager or stdin
--no-video Only play audio when playing a video. Disabled by default
--use-system-mpv-config Use system mpv config instead of no config. Disabled by default
--upscale-images Upscale low-resolution manga pages using waifu2x-ncnn-vulkan. Disabled by default
diff --git a/TODO b/TODO
index 3431697..2e5fc8e 100644
--- a/TODO
+++ b/TODO
@@ -197,4 +197,6 @@ Use event timestamp to sort display name/room name (etc) events to apply them in
Restart and resume youtube media download in downloader when media is throttled.
Try to reconnect media proxy on disconnect. The internet may be unstable (for example on mobile internet).
Youtube is still getting throttled, but to 500kb/sec from 5mb/sec. How to detect this???.
-Opening a media url should display it directly in quickmedia. \ No newline at end of file
+Opening a media url should display it directly in quickmedia.
+Automatically resize body item thumbnail if body is small, or move thumbnail above the text.
+Support &nbsp? \ No newline at end of file
diff --git a/depends/html-search b/depends/html-search
-Subproject 9cafc14e2508224c901f5a03bb6fda73f0c51e5
+Subproject 95c189f7445e6deca85130b7b8fa25dc76fabe1
diff --git a/images/mal_logo.png b/images/mal_logo.png
new file mode 100644
index 0000000..232d983
--- /dev/null
+++ b/images/mal_logo.png
Binary files differ
diff --git a/include/StringUtils.hpp b/include/StringUtils.hpp
index 888adbb..cc2acf4 100644
--- a/include/StringUtils.hpp
+++ b/include/StringUtils.hpp
@@ -21,4 +21,5 @@ namespace QuickMedia {
size_t str_find_case_insensitive(const std::string &str, size_t start_index, const char *substr, size_t substr_len);
char to_upper(char c);
bool strcase_equals(const char *str1, const char *str2);
+ bool to_num(const char *str, size_t size, int &num);
} \ No newline at end of file
diff --git a/plugins/HotExamples.hpp b/plugins/HotExamples.hpp
index 154f1ab..73bc150 100644
--- a/plugins/HotExamples.hpp
+++ b/plugins/HotExamples.hpp
@@ -10,7 +10,7 @@ namespace QuickMedia {
HotExamplesLanguageSelectPage(Program *program) : Page(program) {}
const char* get_title() const override { return "Select language"; }
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- bool submit_is_async() override { return false; }
+ bool submit_is_async() const override { return false; }
};
class HotExamplesSearchPage : public Page {
@@ -29,7 +29,7 @@ namespace QuickMedia {
HotExamplesCodeExamplesPage(Program *program, std::string title) : Page(program), title(std::move(title)) {}
const char* get_title() const override { return title.c_str(); }
PluginResult submit(const std::string&, const std::string&, std::vector<Tab>&) override { return PluginResult::OK; }
- bool submit_is_async() override { return false; }
+ bool submit_is_async() const override { return false; }
private:
std::string title;
};
diff --git a/plugins/Info.hpp b/plugins/Info.hpp
index 4ab39f5..af62282 100644
--- a/plugins/Info.hpp
+++ b/plugins/Info.hpp
@@ -8,7 +8,7 @@ namespace QuickMedia {
InfoPage(Program *program) : Page(program) {}
const char* get_title() const override { return "Info"; }
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- bool submit_is_async() override { return false; }
+ bool submit_is_async() const override { return false; }
static std::shared_ptr<BodyItem> add_url(const std::string &url);
static std::shared_ptr<BodyItem> add_reverse_image_search(const std::string &image_url);
diff --git a/plugins/MangaCombined.hpp b/plugins/MangaCombined.hpp
index dafc884..39a143c 100644
--- a/plugins/MangaCombined.hpp
+++ b/plugins/MangaCombined.hpp
@@ -22,7 +22,6 @@ namespace QuickMedia {
SearchResult search(const std::string &str, BodyItems &result_items) override;
PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); };
void cancel_operation() override;
private:
std::vector<MangaPlugin> search_pages;
diff --git a/plugins/MangaGeneric.hpp b/plugins/MangaGeneric.hpp
index dcd3544..2f65762 100644
--- a/plugins/MangaGeneric.hpp
+++ b/plugins/MangaGeneric.hpp
@@ -113,7 +113,6 @@ namespace QuickMedia {
PluginResult get_page(const std::string &url, bool is_post, const std::vector<MangaFormDataStr> &form_data, BodyItems &result_items);
PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); };
MangaGenericSearchPage& search_handler(const char *search_template, int page_start);
MangaGenericSearchPage& search_post_handler(const char *url, std::vector<MangaFormData> form_data, SearchQueryJsonHandler result_handler);
@@ -175,7 +174,6 @@ namespace QuickMedia {
const char* get_title() const override { return creator.name.c_str(); }
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
PluginResult lazy_fetch(BodyItems &result_items) override;
- sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); };
private:
MangaGenericSearchPage *search_page;
Creator creator;
diff --git a/plugins/Mangadex.hpp b/plugins/Mangadex.hpp
index 7497b06..cd81c44 100644
--- a/plugins/Mangadex.hpp
+++ b/plugins/Mangadex.hpp
@@ -16,7 +16,6 @@ namespace QuickMedia {
SearchResult search(const std::string &str, BodyItems &result_items) override;
PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); };
ChapterImageUrls chapter_image_urls;
private:
diff --git a/plugins/Manganelo.hpp b/plugins/Manganelo.hpp
index 7cf3420..62fd9bf 100644
--- a/plugins/Manganelo.hpp
+++ b/plugins/Manganelo.hpp
@@ -10,7 +10,6 @@ namespace QuickMedia {
bool search_is_filter() override { return false; }
SearchResult search(const std::string &str, BodyItems &result_items) override;
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); };
};
class ManganeloChaptersPage : public MangaChaptersPage {
@@ -28,7 +27,6 @@ namespace QuickMedia {
const char* get_title() const override { return creator.name.c_str(); }
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
PluginResult lazy_fetch(BodyItems &result_items) override;
- sf::Vector2i get_thumbnail_max_size() override { return sf::Vector2i(101, 141); };
private:
Creator creator;
};
diff --git a/plugins/Matrix.hpp b/plugins/Matrix.hpp
index f589db5..5292414 100644
--- a/plugins/Matrix.hpp
+++ b/plugins/Matrix.hpp
@@ -319,7 +319,7 @@ namespace QuickMedia {
const char* get_title() const override { return title.c_str(); }
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- bool submit_is_async() override { return false; }
+ bool submit_is_async() const override { return false; }
bool clear_search_after_submit() override { return true; }
void add_body_item(std::shared_ptr<BodyItem> body_item);
@@ -349,7 +349,7 @@ namespace QuickMedia {
MatrixRoomTagsPage(Program *program, Body *body) : Page(program), body(body) {}
const char* get_title() const override { return "Tags"; }
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- bool submit_is_async() override { return false; }
+ bool submit_is_async() const override { return false; }
bool clear_search_after_submit() override { return true; }
void add_room_body_item_to_tag(std::shared_ptr<BodyItem> body_item, const std::string &tag);
@@ -380,7 +380,7 @@ namespace QuickMedia {
const char* get_title() const override { return title.c_str(); }
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- bool submit_is_async() override { return false; }
+ bool submit_is_async() const override { return false; }
bool clear_search_after_submit() override { return true; }
void add_body_item(std::shared_ptr<BodyItem> body_item);
@@ -484,7 +484,7 @@ namespace QuickMedia {
PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
PluginResult lazy_fetch(BodyItems &result_items) override;
bool is_ready() override;
- bool submit_is_async() override { return false; }
+ bool submit_is_async() const override { return false; }
bool clear_search_after_submit() override { return true; }
void add_notification(MatrixNotification notification);
diff --git a/plugins/MediaGeneric.hpp b/plugins/MediaGeneric.hpp
index 8885db2..fce0e97 100644
--- a/plugins/MediaGeneric.hpp
+++ b/plugins/MediaGeneric.hpp
@@ -40,7 +40,6 @@ namespace QuickMedia {
SearchResult search(const std::string &str, BodyItems &result_items) override;
PluginResult get_page(const std::string &str, int page, BodyItems &result_items) override;
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- sf::Vector2i get_thumbnail_max_size() override { return thumbnail_max_size; };
PluginResult get_related_media(const std::string &url, BodyItems &result_items);
diff --git a/plugins/MyAnimeList.hpp b/plugins/MyAnimeList.hpp
new file mode 100644
index 0000000..c88e94f
--- /dev/null
+++ b/plugins/MyAnimeList.hpp
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "Page.hpp"
+
+namespace QuickMedia {
+ class MyAnimeListSearchPage : public Page {
+ public:
+ MyAnimeListSearchPage(Program *program) : Page(program) {}
+ const char* get_title() const override { return "Search for anime or manga"; }
+ bool search_is_filter() override { return false; }
+ SearchResult search(const std::string &str, BodyItems &result_items) override;
+ PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ };
+
+ class MyAnimeListDetailsPage : public LazyFetchPage {
+ public:
+ MyAnimeListDetailsPage(Program *program, std::string url) : LazyFetchPage(program), url(std::move(url)) {}
+ const char* get_title() const override { return "Details"; }
+ bool submit_is_async() const override { return false; }
+ PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ private:
+ std::string url;
+ };
+
+ class MyAnimeListRecommendationsPage : public LazyFetchPage {
+ public:
+ MyAnimeListRecommendationsPage(Program *program, std::string url) : LazyFetchPage(program), url(std::move(url)) {}
+ const char* get_title() const override { return "Recommendations"; }
+ bool submit_is_async() const override { return false; }
+ PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
+ PluginResult lazy_fetch(BodyItems &result_items) override;
+ private:
+ std::string url;
+ };
+} \ No newline at end of file
diff --git a/plugins/NyaaSi.hpp b/plugins/NyaaSi.hpp
index 6f161ac..f113544 100644
--- a/plugins/NyaaSi.hpp
+++ b/plugins/NyaaSi.hpp
@@ -54,7 +54,7 @@ namespace QuickMedia {
NyaaSiSortOrderPage(Program *program, Body *body, NyaaSiSearchPage *search_page) : Page(program), body(body), search_page(search_page) {}
const char* get_title() const override { return "Sort order"; }
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- bool submit_is_async() override { return false; }
+ bool submit_is_async() const override { return false; }
private:
Body *body;
NyaaSiSearchPage *search_page;
@@ -65,6 +65,6 @@ namespace QuickMedia {
NyaaSiTorrentPage(Program *program) : Page(program) {}
const char* get_title() const override { return "Torrent"; }
PluginResult submit(const std::string &title, const std::string &url, std::vector<Tab> &result_tabs) override;
- bool submit_is_async() override { return false; }
+ bool submit_is_async() const override { return false; }
};
} \ No newline at end of file
diff --git a/plugins/Page.hpp b/plugins/Page.hpp
index 9d5bade..8d033e1 100644
--- a/plugins/Page.hpp
+++ b/plugins/Page.hpp
@@ -41,7 +41,7 @@ namespace QuickMedia {
return PluginResult::ERR;
}
// Override and return false to make submit run in the main (ui) thread
- virtual bool submit_is_async() { return true; }
+ virtual bool submit_is_async() const { return true; }
virtual bool clear_search_after_submit() { return false; }
virtual PluginResult submit_suggestion(const std::string &title, const std::string &url, BodyItems &result_items) {
(void)title;
@@ -73,8 +73,6 @@ namespace QuickMedia {
std::unique_ptr<SearchBar> create_search_bar(const std::string &placeholder_text, int search_delay);
bool load_manga_content_storage(const char *service_name, const std::string &manga_title, const std::string &manga_id);
-
- virtual sf::Vector2i get_thumbnail_max_size() { return sf::Vector2i(250, 141); };
Program *program;
std::shared_ptr<BodyItem> submit_body_item; // TODO: Remove this
diff --git a/src/Body.cpp b/src/Body.cpp
index c3cbcda..b7424c5 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -115,8 +115,8 @@ namespace QuickMedia {
embedded_item_load_text = sf::Text("", *FontLoader::get_font(FontLoader::FontType::LATIN), body_spacing[body_theme].embedded_item_font_size);
progress_text.setFillColor(get_current_theme().text_color);
replies_text.setFillColor(get_current_theme().replies_text_color);
- thumbnail_max_size.x = 250;
- thumbnail_max_size.y = 141;
+ thumbnail_max_size.x = 600;
+ thumbnail_max_size.y = 337;
sf::Vector2f loading_icon_size(loading_icon.getTexture()->getSize().x, loading_icon.getTexture()->getSize().y);
loading_icon.setOrigin(loading_icon_size.x * 0.5f, loading_icon_size.y * 0.5f);
render_selected_item_bg = !is_touch_enabled();
@@ -923,7 +923,7 @@ namespace QuickMedia {
if(item->thumbnail_size.x > 0 && item->thumbnail_size.y > 0)
content_size = clamp_to_size(sf::Vector2i(std::floor(item->thumbnail_size.x * get_ui_scale()), std::floor(item->thumbnail_size.y * get_ui_scale())), thumbnail_max_size_scaled);
else
- content_size = thumbnail_max_size_scaled;
+ content_size = sf::Vector2i(250 * get_ui_scale(), 141 * get_ui_scale());
return content_size;
}
diff --git a/src/NetUtils.cpp b/src/NetUtils.cpp
index 28256cb..0f957d5 100644
--- a/src/NetUtils.cpp
+++ b/src/NetUtils.cpp
@@ -34,17 +34,6 @@ namespace QuickMedia {
std::string unescaped_str;
};
- static bool to_num(const char *str, size_t size, int &num) {
- num = 0;
- for(size_t i = 0; i < size; ++i) {
- const char num_c = str[i] - '0';
- if(num_c < 0 || num_c > 9)
- return false;
- num = (num * 10) + num_c;
- }
- return true;
- }
-
static void html_unescape_sequence_numbers(std::string &str) {
size_t index = 0;
while(true) {
@@ -69,10 +58,13 @@ namespace QuickMedia {
void html_unescape_sequences(std::string &str) {
html_unescape_sequence_numbers(str);
- const std::array<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