aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--README.md4
-rw-r--r--images/mangadex_logo.pngbin0 -> 4094 bytes
-rw-r--r--include/Notification.hpp32
-rw-r--r--include/QuickMedia.hpp4
-rw-r--r--plugins/Manga.hpp19
-rw-r--r--plugins/Mangadex.hpp31
-rw-r--r--plugins/Manganelo.hpp16
-rw-r--r--plugins/Plugin.hpp1
-rw-r--r--src/Body.cpp1
-rw-r--r--src/Program.c24
-rw-r--r--src/QuickMedia.cpp133
-rw-r--r--src/Storage.cpp8
-rw-r--r--src/plugins/Mangadex.cpp377
-rw-r--r--src/plugins/Manganelo.cpp34
14 files changed, 585 insertions, 99 deletions
diff --git a/README.md b/README.md
index 35b57c5..ee5acc7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,6 @@
# QuickMedia
Native clients of websites with fast access to what you want to see, **with TOR support**. See [old video demo with manga](https://lbry.tv/quickmedia_manga-2019-08-05_21.20.46/7).\
-Currently supported websites: `youtube`, `manganelo`, `4chan` and _others_.\
+Currently supported websites: `youtube`, `manganelo`, `mangadex`, `4chan` and _others_.\
**Note:** Manganelo doesn't work when used with TOR.\
**Note:** Posting comments on 4chan doesn't work when used with TOR. However browing works.\
**Note:** TOR system service needs to be running (`systemctl start tor.service`).\
@@ -11,7 +11,7 @@ Config data, including manga progress is stored under `$HOME/.config/quickmedia`
```
usage: QuickMedia <plugin> [--tor]
OPTIONS:
-plugin The plugin to use. Should be either 4chan, manganelo or youtube
+plugin The plugin to use. Should be either 4chan, manganelo, mangadex or youtube
--tor Use tor. Disabled by default
EXAMPLES:
QuickMedia manganelo
diff --git a/images/mangadex_logo.png b/images/mangadex_logo.png
new file mode 100644
index 0000000..698a879
--- /dev/null
+++ b/images/mangadex_logo.png
Binary files differ
diff --git a/include/Notification.hpp b/include/Notification.hpp
new file mode 100644
index 0000000..22f2f77
--- /dev/null
+++ b/include/Notification.hpp
@@ -0,0 +1,32 @@
+#pragma once
+
+#include "Program.h"
+#include <assert.h>
+#include <string>
+
+namespace QuickMedia {
+ enum class Urgency {
+ LOW,
+ NORMAL,
+ CRITICAL
+ };
+
+ static const char* urgency_string(Urgency urgency) {
+ switch(urgency) {
+ case Urgency::LOW:
+ return "low";
+ case Urgency::NORMAL:
+ return "normal";
+ case Urgency::CRITICAL:
+ return "critical";
+ }
+ assert(false);
+ return nullptr;
+ }
+
+ static void show_notification(const std::string &title, const std::string &description, Urgency urgency = Urgency::NORMAL) {
+ const char *args[] = { "notify-send", "-u", urgency_string(urgency), "--", title.c_str(), description.c_str(), nullptr };
+ exec_program_async(args, nullptr);
+ printf("Notification: title: %s, description: %s\n", title.c_str(), description.c_str());
+ }
+} \ No newline at end of file
diff --git a/include/QuickMedia.hpp b/include/QuickMedia.hpp
index bd72806..19d84a8 100644
--- a/include/QuickMedia.hpp
+++ b/include/QuickMedia.hpp
@@ -15,7 +15,7 @@
namespace QuickMedia {
class Plugin;
- class Manganelo;
+ class Manga;
class Program {
public:
@@ -43,7 +43,7 @@ namespace QuickMedia {
};
LoadImageResult load_image_by_index(int image_index, sf::Texture &image_texture, sf::String &error_message);
- void download_chapter_images_if_needed(Manganelo *image_plugin);
+ void download_chapter_images_if_needed(Manga *image_plugin);
void select_episode(BodyItem *item, bool start_from_beginning);
// Returns Page::EXIT if empty
diff --git a/plugins/Manga.hpp b/plugins/Manga.hpp
new file mode 100644
index 0000000..18ed2f9
--- /dev/null
+++ b/plugins/Manga.hpp
@@ -0,0 +1,19 @@
+#pragma once
+
+#include "Plugin.hpp"
+#include <functional>
+#include <mutex>
+
+namespace QuickMedia {
+ // Return false to stop iteration
+ using PageCallback = std::function<bool(const std::string &url)>;
+
+ class Manga : public Plugin {
+ public:
+ Manga(const std::string &plugin_name) : Plugin(plugin_name) {}
+ bool is_manga() override { return true; }
+ virtual ImageResult get_number_of_images(const std::string &url, int &num_images) = 0;
+ virtual ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) = 0;
+ virtual bool extract_id_from_url(const std::string &url, std::string &manga_id) = 0;
+ };
+} \ No newline at end of file
diff --git a/plugins/Mangadex.hpp b/plugins/Mangadex.hpp
new file mode 100644
index 0000000..9d6d366
--- /dev/null
+++ b/plugins/Mangadex.hpp
@@ -0,0 +1,31 @@
+#pragma once
+
+#include "Manga.hpp"
+#include <functional>
+#include <mutex>
+
+namespace QuickMedia {
+ class Mangadex : public Manga {
+ public:
+ Mangadex() : Manga("mangadex") {}
+ SearchResult search(const std::string &url, BodyItems &result_items) override;
+ SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override;
+ ImageResult get_number_of_images(const std::string &url, int &num_images) override;
+ bool search_suggestions_has_thumbnails() const override { return true; }
+ bool search_results_has_thumbnails() const override { return false; }
+ int get_search_delay() const override { return 300; }
+ Page get_page_after_search() const override { return Page::EPISODE_LIST; }
+
+ ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) override;
+
+ bool extract_id_from_url(const std::string &url, std::string &manga_id) override;
+ private:
+ SearchResult search_page(const std::string &url, BodyItems &result_items, int page, bool *is_last_page);
+ // Caches url. If the same url is requested multiple times then the cache is used
+ ImageResult get_image_urls_for_chapter(const std::string &url);
+ private:
+ std::string last_chapter_url;
+ std::vector<std::string> last_chapter_image_urls;
+ std::mutex image_urls_mutex;
+ };
+} \ No newline at end of file
diff --git a/plugins/Manganelo.hpp b/plugins/Manganelo.hpp
index 3405de2..ffac830 100644
--- a/plugins/Manganelo.hpp
+++ b/plugins/Manganelo.hpp
@@ -1,26 +1,24 @@
#pragma once
-#include "Plugin.hpp"
+#include "Manga.hpp"
#include <functional>
#include <mutex>
namespace QuickMedia {
- // Return false to stop iteration
- using PageCallback = std::function<bool(const std::string &url)>;
-
- class Manganelo : public Plugin {
+ class Manganelo : public Manga {
public:
- Manganelo() : Plugin("manganelo") {}
+ Manganelo() : Manga("manganelo") {}
SearchResult search(const std::string &url, BodyItems &result_items) override;
SuggestionResult update_search_suggestions(const std::string &text, BodyItems &result_items) override;
- ImageResult get_image_by_index(const std::string &url, int index, std::string &image_data);
- ImageResult get_number_of_images(const std::string &url, int &num_images);
+ ImageResult get_number_of_images(const std::string &url, int &num_images) override;
bool search_suggestions_has_thumbnails() const override { return true; }
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::EPISODE_LIST; }
- ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback);
+ ImageResult for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) override;
+
+ bool extract_id_from_url(const std::string &url, std::string &manga_id) override;
private:
// Caches url. If the same url is requested multiple times then the cache is used
ImageResult get_image_urls_for_chapter(const std::string &url);
diff --git a/plugins/Plugin.hpp b/plugins/Plugin.hpp
index e19fbcd..b430d57 100644
--- a/plugins/Plugin.hpp
+++ b/plugins/Plugin.hpp
@@ -42,6 +42,7 @@ namespace QuickMedia {
virtual ~Plugin() = default;
virtual bool is_image_board() { return false; }
+ virtual bool is_manga() { return false; }
virtual PluginResult get_front_page(BodyItems &result_items) {
(void)result_items; return PluginResult::OK;
diff --git a/src/Body.cpp b/src/Body.cpp
index c892c4e..8ccb3cc 100644
--- a/src/Body.cpp
+++ b/src/Body.cpp
@@ -112,6 +112,7 @@ namespace QuickMedia {
loading_thumbnail = true;
thumbnail_load_thread = std::thread([this, result, url]() {
std::string texture_data;
+ // TODO: Cache images instead of redownloading them everytime they appear on the screen
if(download_to_string(url, texture_data, {}, program->get_current_plugin()->use_tor) == DownloadResult::OK) {
if(result->loadFromMemory(texture_data.data(), texture_data.size())) {
//result->generateMipmap();
diff --git a/src/Program.c b/src/Program.c
index 3246d54..2509f6f 100644
--- a/src/Program.c
+++ b/src/Program.c
@@ -1,9 +1,11 @@
#include "../include/Program.h"
#include <unistd.h>
#include <sys/wait.h>
+#include <sys/prctl.h>
#include <errno.h>
#include <string.h>
#include <stdio.h>
+#include <stdlib.h>
#include <assert.h>
#define READ_END 0
@@ -20,11 +22,22 @@ int exec_program(const char **args, ProgramOutputCallback output_callback, void
return -2;
}
+ pid_t parent_pid = getpid();
+
pid_t pid = fork();
if(pid == -1) {
perror("Failed to fork");
return -3;
} else if(pid == 0) { /* child */
+ if(prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) {
+ perror("prctl(PR_SET_PDEATHSIG, SIGTERM) failed");
+ exit(127);
+ }
+
+ /* Test if the parent died before the above call to prctl */
+ if(getppid() != parent_pid)
+ exit(127);
+
dup2(fd[WRITE_END], STDOUT_FILENO);
close(fd[READ_END]);
close(fd[WRITE_END]);
@@ -105,12 +118,23 @@ int exec_program_async(const char **args, pid_t *result_process_id) {
if(args[0] == NULL)
return -1;
+ pid_t parent_pid = getpid();
+
pid_t pid = fork();
if(pid == -1) {
int err = errno;
perror("Failed to fork");
return -err;
} else if(pid == 0) { /* child */
+ if(prctl(PR_SET_PDEATHSIG, SIGTERM) == -1) {
+ perror("prctl(PR_SET_PDEATHSIG, SIGTERM) failed");
+ exit(127);
+ }
+
+ /* Test if the parent died before the above call to prctl */
+ if(getppid() != parent_pid)
+ exit(127);
+
execvp(args[0], args);
} else { /* parent */
if(result_process_id)
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index d7c15b1..5a13179 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -1,5 +1,6 @@
#include "../include/QuickMedia.hpp"
#include "../plugins/Manganelo.hpp"
+#include "../plugins/Mangadex.hpp"
#include "../plugins/Youtube.hpp"
#include "../plugins/Pornhub.hpp"
#include "../plugins/Fourchan.hpp"
@@ -8,6 +9,7 @@
#include "../include/VideoPlayer.hpp"
#include "../include/StringUtils.hpp"
#include "../include/GoogleCaptcha.hpp"
+#include "../include/Notification.hpp"
#include <cppcodec/base64_rfc4648.hpp>
#include <SFML/Graphics/RectangleShape.hpp>
@@ -91,7 +93,7 @@ namespace QuickMedia {
static void usage() {
fprintf(stderr, "usage: QuickMedia <plugin> [--tor]\n");
fprintf(stderr, "OPTIONS:\n");
- fprintf(stderr, "plugin The plugin to use. Should be either 4chan, manganelo, pornhub or youtube\n");
+ fprintf(stderr, "plugin The plugin to use. Should be either 4chan, manganelo, mangadex, pornhub or youtube\n");
fprintf(stderr, "--tor Use tor. Disabled by default\n");
fprintf(stderr, "EXAMPLES:\n");
fprintf(stderr, "QuickMedia manganelo\n");
@@ -132,6 +134,9 @@ namespace QuickMedia {
if(strcmp(argv[i], "manganelo") == 0) {
current_plugin = new Manganelo();
plugin_logo_path = "../../../images/manganelo_logo.png";
+ } else if(strcmp(argv[i], "mangadex") == 0) {
+ current_plugin = new Mangadex();
+ plugin_logo_path = "../../../images/mangadex_logo.png";
} else if(strcmp(argv[i], "youtube") == 0) {
current_plugin = new Youtube();
plugin_logo_path = "../../../images/yt_logo_rgb_dark_small.png";
@@ -261,31 +266,6 @@ namespace QuickMedia {
}
}
- enum class Urgency {
- LOW,
- NORMAL,
- CRITICAL
- };
-
- const char* urgency_string(Urgency urgency) {
- switch(urgency) {
- case Urgency::LOW:
- return "low";
- case Urgency::NORMAL:
- return "normal";
- case Urgency::CRITICAL:
- return "critical";
- }
- assert(false);
- return nullptr;
- }
-
- static void show_notification(const std::string &title, const std::string &description, Urgency urgency = Urgency::NORMAL) {
- const char *args[] = { "notify-send", "-u", urgency_string(urgency), "--", title.c_str(), description.c_str(), nullptr };
- exec_program_async(args, nullptr);
- printf("Notification: title: %s, description: %s\n", title.c_str(), description.c_str());
- }
-
static std::string base64_encode(const std::string &data) {
return cppcodec::base64_rfc4648::encode(data);
}
@@ -315,39 +295,6 @@ namespace QuickMedia {
return file_overwrite(path, Json::writeString(json_builder, json)) == 0;
}
- 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 || url.find("manganelo") != std::string::npos)
- manganelo_website = true;
-
- if(manganelo_website) {
- size_t index = url.find("manga/");
- if(index == std::string::npos) {
- std::string err_msg = "Url ";
- err_msg += url;
- err_msg += " doesn't contain manga id";
- show_notification("Manga", err_msg, Urgency::CRITICAL);
- return false;
- }
-
- 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);
- return false;
- }
- 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);
- return false;
- }
- }
-
enum class SearchSuggestionTab {
ALL,
HISTORY
@@ -373,14 +320,20 @@ namespace QuickMedia {
int selected_tab = 0;
// TOOD: Make generic, instead of checking for plugin
- if(current_plugin->name == "manganelo") {
- Path content_storage_dir = get_storage_dir().join("manga");
+ if(current_plugin->is_manga()) {
+ Path content_storage_dir = get_storage_dir().join(current_plugin->name);
if(create_directory_recursive(content_storage_dir) != 0) {
show_notification("Storage", "Failed to create directory: " + content_storage_dir.data, Urgency::CRITICAL);
exit(1);
}
+ Path credentials_storage_dir = get_storage_dir().join("credentials");
+ if(create_directory_recursive(credentials_storage_dir) != 0) {
+ show_notification("Storage", "Failed to create directory: " + credentials_storage_dir.data, Urgency::CRITICAL);
+ exit(1);
+ }
// TODO: Make asynchronous
- for_files_in_dir_sort_last_modified(content_storage_dir, [&history_body](const std::filesystem::path &filepath) {
+ // TODO: Make this also work for mangadex. Would require storing both id and name of the manga
+ for_files_in_dir_sort_last_modified(content_storage_dir, [&history_body, this](const std::filesystem::path &filepath) {
Path fullpath(filepath.c_str());
Json::Value body;
if(!read_file_as_json(fullpath, body)) {
@@ -393,7 +346,10 @@ namespace QuickMedia {
if(!filename.empty() && manga_name.isString()) {
// TODO: Add thumbnail
auto body_item = std::make_unique<BodyItem>(manga_name.asString());
- body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string());
+ if(current_plugin->name == "manganelo")
+ body_item->url = "https://manganelo.com/manga/" + base64_decode(filename.string());
+ else if(current_plugin->name == "mangadex")
+ body_item->url = "https://mangadex.org/title/" + base64_decode(filename.string());
history_body.items.push_back(std::move(body_item));
}
return true;
@@ -417,16 +373,17 @@ namespace QuickMedia {
return false;
}
- if(next_page == Page::EPISODE_LIST) {
+ if(next_page == Page::EPISODE_LIST && current_plugin->is_manga()) {
+ Manga *manga_plugin = static_cast<Manga*>(current_plugin);
if(content_url.empty()) {
show_notification("Manga", "Url is missing for manga!", Urgency::CRITICAL);
return false;
}
- Path content_storage_dir = get_storage_dir().join("manga");
+ Path content_storage_dir = get_storage_dir().join(current_plugin->name);
std::string manga_id;
- if(!manga_extract_id_from_url(content_url, manga_id))
+ if(!manga_plugin->extract_id_from_url(content_url, manga_id))
return false;
manga_id_base64 = base64_encode(manga_id);
@@ -513,7 +470,9 @@ namespace QuickMedia {
if(!update_search_text.empty() && !search_running) {
search_suggestion_future = std::async(std::launch::async, [this, update_search_text]() {
BodyItems result;
- SuggestionResult suggestion_result = current_plugin->update_search_suggestions(update_search_text, result);
+ if(current_plugin->update_search_suggestions(update_search_text, result) != SuggestionResult::OK) {
+ show_notification("Search", "Search failed!", Urgency::CRITICAL);
+ }
return result;
});
update_search_text.clear();
@@ -1004,7 +963,7 @@ namespace QuickMedia {
return LoadImageResult::FAILED;
}
} else {
- show_notification("Manganelo", "Failed to load image for page " + std::to_string(image_index + 1) + ". Image filepath: " + image_path.data, Urgency::CRITICAL);
+ show_notification("Manga", "Failed to load image for page " + std::to_string(image_index + 1) + ". Image filepath: " + image_path.data, Urgency::CRITICAL);
error_message = std::string("Failed to load image for page ") + std::to_string(image_index + 1);
return LoadImageResult::FAILED;
}
@@ -1014,7 +973,7 @@ namespace QuickMedia {
}
}
- void Program::download_chapter_images_if_needed(Manganelo *image_plugin) {
+ void Program::download_chapter_images_if_needed(Manga *image_plugin) {
if(downloading_chapter_url == images_url)
return;
@@ -1044,6 +1003,7 @@ namespace QuickMedia {
Path image_filepath = content_cache_dir_;
image_filepath.join(image_filename);
#endif
+ // TODO: Save image with the file extension that url says it has? right now the file is saved without any extension
Path image_filepath = content_cache_dir_;
image_filepath.join(std::to_string(page++));
@@ -1053,22 +1013,27 @@ namespace QuickMedia {
std::string image_content;
if(download_to_string(url, image_content, {}, current_plugin->use_tor) != DownloadResult::OK || image_content.size() <= 255) {
- bool try_backup_url = false;
- std::string new_url = url;
- if(string_replace_all(new_url, "s3.mkklcdnv3.com", "bu.mkklcdnbuv1.com") > 0) {
- try_backup_url = true;
- } else {
- try_backup_url = (string_replace_all(new_url, "s41.mkklcdnv41.com", "bu.mkklcdnbuv1.com") > 0);
- }
+ if(current_plugin->name == "manganelo") {
+ bool try_backup_url = false;
+ std::string new_url = url;
+ if(string_replace_all(new_url, "s3.mkklcdnv3.com", "bu.mkklcdnbuv1.com") > 0) {
+ try_backup_url = true;
+ } else {
+ try_backup_url = (string_replace_all(new_url, "s41.mkklcdnv41.com", "bu.mkklcdnbuv1.com") > 0);
+ }
- if(try_backup_url) {
- image_content.clear();
- if(download_to_string(new_url, image_content, {}, current_plugin->use_tor) != DownloadResult::OK || image_content.size() <= 255) {
- show_notification("Manganelo", "Failed to download image: " + new_url, Urgency::CRITICAL);
+ if(try_backup_url) {
+ image_content.clear();
+ if(download_to_string(new_url, image_content, {}, current_plugin->use_tor) != DownloadResult::OK || image_content.size() <= 255) {
+ show_notification("Manganelo", "Failed to download image: " + new_url, Urgency::CRITICAL);
+ return false;
+ }
+ } else {
+ show_notification("Manganelo", "Failed to download image: " + url, Urgency::CRITICAL);
return false;
}
} else {
- show_notification("Manganelo", "Failed to download image: " + url, Urgency::CRITICAL);
+ show_notification("Manga", "Failed to download image: " + url, Urgency::CRITICAL);
return false;
}
}
@@ -1096,12 +1061,12 @@ namespace QuickMedia {
sf::Text error_message("", font, 30);
error_message.setFillColor(sf::Color::White);
- assert(current_plugin->name == "manganelo");
- Manganelo *image_plugin = static_cast<Manganelo*>(current_plugin);
+ assert(current_plugin->is_manga());
+ Manga *image_plugin = static_cast<Manga*>(current_plugin);
std::string image_data;
bool download_in_progress = false;
- content_cache_dir = get_cache_dir().join("manga").join(manga_id_base64).join(base64_encode(chapter_title));
+ content_cache_dir = get_cache_dir().join(image_plugin->name).join(manga_id_base64).join(base64_encode(chapter_title));
if(create_directory_recursive(content_cache_dir) != 0) {
show_notification("Storage", "Failed to create directory: " + content_cache_dir.data, Urgency::CRITICAL);
current_page = Page::EPISODE_LIST;
diff --git a/src/Storage.cpp b/src/Storage.cpp
index 919044e..745ecfc 100644
--- a/src/Storage.cpp
+++ b/src/Storage.cpp
@@ -89,13 +89,17 @@ namespace QuickMedia {
}
int file_get_content(const Path &path, std::string &result) {
- assert(get_file_type(path) == FileType::REGULAR);
FILE *file = fopen(path.data.c_str(), "rb");
if(!file)
return -errno;
fseek(file, 0, SEEK_END);
- size_t file_size = ftell(file);
+ long file_size = ftell(file);
+ if(file_size == -1) {
+ fprintf(stderr, "Error: attempted to read directory %s as a file\n", path.data.c_str());
+ fclose(file);
+ return -1;
+ }
fseek(file, 0, SEEK_SET);
result.resize(file_size);
diff --git a/src/plugins/Mangadex.cpp b/src/plugins/Mangadex.cpp
new file mode 100644
index 0000000..43368af
--- /dev/null
+++ b/src/plugins/Mangadex.cpp
@@ -0,0 +1,377 @@
+#include "../../plugins/Mangadex.hpp"
+#include "../../include/Storage.hpp"
+#include <quickmedia/HtmlSearch.h>
+#include <json/reader.h>
+
+static const std::string mangadex_url = "https://mangadex.org";
+// TODO: Allow selecting other languages than english
+static const char *language_code = "1"; // english
+static const std::string useragent_str = "user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/81.0.4044.138 Safari/537.36";
+
+namespace QuickMedia {
+ static std::string title_url_extract_manga_id(const std::string &url) {
+ size_t find_index = url.find("/title/");
+ if(find_index == std::string::npos)
+ return "";
+
+ size_t id_start_index = find_index + 7;
+ size_t end_index = url.find("/", id_start_index);
+ if(end_index == std::string::npos)
+ return url.substr(id_start_index);
+
+ return url.substr(id_start_index, end_index - id_start_index);
+ }
+
+ static std::string chapter_url_extract_manga_id(const std::string &url) {
+ size_t find_index = url.find("/chapter/");
+ if(find_index == std::string::npos)
+ return "";
+ return url.substr(find_index + 9);
+ }
+
+ struct BodyItemChapterContext {
+ BodyItems *body_items;
+ int prev_chapter_number;
+ bool *is_last_page;
+ };
+
+ SearchResult Mangadex::search_page(const std::string &url, BodyItems &result_items, int page, bool *is_last_page) {
+ *is_last_page = true;
+ CommandArg user_agent_arg = { "-H", useragent_str };
+
+ std::string chapter_url = url;
+ if(chapter_url[0] != '/')
+ chapter_url += "/";
+ chapter_url += "chapters/" + std::to_string(page) + "/";
+ std::string website_data;
+ if(download_to_string(chapter_url, website_data, {std::move(user_agent_arg)}, use_tor) != DownloadResult::OK)
+ return SearchResult::NET_ERR;
+
+ std::string manga_id = title_url_extract_manga_id(chapter_url);
+ std::string query = "//div[data-manga-id='" + manga_id + "']";
+
+ BodyItemChapterContext body_item_chapter_context;
+ body_item_chapter_context.body_items = &result_items;
+ body_item_chapter_context.prev_chapter_number = -1;
+ body_item_chapter_context.is_last_page = is_last_page;
+
+ QuickMediaHtmlSearch html_search;
+ int result = quickmedia_html_search_init(&html_search, website_data.c_str());
+ if(result != 0)
+ goto cleanup;
+
+ result = quickmedia_html_find_nodes_xpath(&html_search, query.c_str(),
+ [](QuickMediaHtmlNode *node, void *userdata) {
+ auto *item_data = (BodyItemChapterContext*)userdata;
+ const char *data_lang = quickmedia_html_node_get_attribute_value(node, "data-lang");
+ if(strcmp(data_lang, language_code) != 0)
+ return;
+
+ const char *chapter_id = quickmedia_html_node_get_attribute_value(node, "data-id");
+ if(!chapter_id)
+ return;
+
+ const char *chapter_number_str = quickmedia_html_node_get_attribute_value(node, "data-chapter");
+ if(!chapter_number_str)
+ return;
+
+ int chapter_number = atoi(chapter_number_str);
+ if(chapter_number == 0 || chapter_number == item_data->prev_chapter_number)
+ return;
+
+ item_data->prev_chapter_number = chapter_number;
+
+ const char *chapter_title = quickmedia_html_node_get_attribute_value(node, "data-title");
+ std::string chapter_url = mangadex_url + "/chapter/" + chapter_id;
+ std::string chapter_name = std::string("Ch. ") + chapter_number_str;
+ if(chapter_title)
+ chapter_name += std::string(" - ") + chapter_title;
+
+ auto item = std::make_unique<BodyItem>(std::move(chapter_name));
+ item->url = std::move(chapter_url);
+ item_data->body_items->push_back(std::move(item));
+ *item_data->is_last_page = false;
+ }, &body_item_chapter_context);
+
+ cleanup:
+ quickmedia_html_search_deinit(&html_search);
+ return result == 0 ? SearchResult::OK : SearchResult::ERR;
+ }
+
+ // TODO: Make pagination asynchronous and make it go to the next page when navigating to the bottom in the list of chapters
+ // in the GUI. Currently all pages are fetched at once, synchronously. This can be very slow for certain manga like Naruto
+ // which has 21 pages of chapters...
+ SearchResult Mangadex::search(const std::string &url, BodyItems &result_items) {
+#if 0
+ int page = 1;
+ while(true) {
+ bool is_last_page;
+ SearchResult search_result = search_page(url, result_items, page, &is_last_page);
+ if(search_result != SearchResult::OK)
+ return search_result;
+
+ ++page;
+ if(is_last_page)
+ break;
+ }
+ return SearchResult::OK;
+#else
+ CommandArg user_agent_arg = { "-H", useragent_str };
+
+ std::string manga_id = title_url_extract_manga_id(url);
+ std::string request_url = "https://mangadex.org/api/?id=" + manga_id + "&type=manga";
+ std::string server_response;
+ if(download_to_string(request_url, server_response, {std::move(user_agent_arg)}, use_tor) != DownloadResult::OK)
+ return SearchResult::NET_ERR;
+
+ if(server_response.empty())
+ return SearchResult::OK;
+
+ 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[0], &server_response[server_response.size()], &json_root, &json_errors)) {
+ fprintf(stderr, "Mangadex search json error: %s\n", json_errors.c_str());
+ return SearchResult::ERR;
+ }
+
+ Json::Value &status_json = json_root["status"];
+ if(!status_json.isString() || status_json.asString() != "OK")
+ return SearchResult::ERR;
+
+ Json::Value &chapter_json = json_root["chapter"];
+ if(!chapter_json.isObject())
+ return SearchResult::ERR;
+
+ std::vector<std::pair<std::string, Json::Value>> chapters(chapter_json.size());
+ for(auto &member_name : chapter_json.getMemberNames()) {
+ Json::Value chapter = chapter_json[member_name];
+ if(chapter.isObject())
+ chapters.push_back(std::make_pair(member_name, std::move(chapter)));
+ }
+
+ std::sort(chapters.begin(), chapters.end(), [](std::pair<std::string, Json::Value> &a, std::pair<std::string, Json::Value> &b) {
+ Json::Value &a_timestamp_json = a.second["timestamp"];
+ Json::Value &b_timestamp_json = b.second["timestamp"];
+ int64_t a_timestamp = 0;
+ int64_t b_timestamp = 0;
+ if(a_timestamp_json.isInt64())
+ a_timestamp = a_timestamp_json.asInt64();
+ if(b_timestamp_json.isInt64())
+ b_timestamp = b_timestamp_json.asInt64();
+ return a_timestamp > b_timestamp;
+ });
+
+ int prev_chapter_number = -1;
+ for(auto it = chapters.begin(); it != chapters.end(); ++it) {
+ const std::string &chapter_id = it->first;
+ Json::Value &chapter = it->second;
+
+ Json::Value &lang_code = chapter["lang_code"];
+ // TODO: Allow selecting other languages than english
+ if(!lang_code.isString() || lang_code.asString() != "gb")
+ continue;
+
+ Json::Value &chapter_number_json = chapter["chapter"];
+ if(!chapter_number_json.isString())
+ continue;
+
+ int chapter_number = atoi(chapter_number_json.asCString());
+ if(chapter_number == 0 || chapter_number == prev_chapter_number)
+ continue;
+ prev_chapter_number = chapter_number;
+
+ Json::Value &chapter_title_json = chapter["title"];
+ std::string chapter_url = mangadex_url + "/chapter/" + chapter_id;
+ std::string chapter_name = std::string("Ch. ") + chapter_number_json.asCString();
+ if(chapter_title_json.isString())
+ chapter_name += std::string(" - ") + chapter_title_json.asCString();
+
+ auto item = std::make_unique<BodyItem>(std::move(chapter_name));
+ item->url = std::move(chapter_url);
+ result_items.push_back(std::move(item));
+ }
+ return SearchResult::OK;
+#endif
+ }
+
+ static bool get_rememberme_token(std::string &rememberme_token) {
+ Path mangadex_credentials_path = get_storage_dir().join("credentials").join("mangadex.json");
+ std::string mangadex_credentials;
+ if(file_get_content(mangadex_credentials_path, mangadex_credentials) != 0) {
+ fprintf(stderr, "Failed to get content of file: %s\n", mangadex_credentials_path.data.c_str());
+ return false;
+ }
+
+ 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(&mangadex_credentials[0], &mangadex_credentials[mangadex_credentials.size()], &json_root, &json_errors)) {
+ fprintf(stderr, "Mangadex credentials json error: %s\n", json_errors.c_str());
+ return false;
+ }
+
+ if(json_root.isObject()) {
+ Json::Value &rememberme_token_json = json_root["rememberme_token"];
+ if(rememberme_token_json.isString()) {
+ rememberme_token = rememberme_token_json.asString();
+ return true;
+ }
+ }
+ return true;
+ }
+
+ struct BodyItemImageContext {
+ BodyItems *body_items;
+ size_t index;
+ };
+
+ // TODO: Implement pagination (go to next page and get all results as well)
+ SuggestionResult Mangadex::update_search_suggestions(const std::string &text, BodyItems &result_items) {
+ std::string rememberme_token;
+ if(!get_rememberme_token(rememberme_token))
+ return SuggestionResult::ERR;
+
+ std::string url = "https://mangadex.org/search?title=";
+ url += url_param_encode(text);
+ CommandArg cookie_arg = { "-H", "cookie: mangadex_rememberme_token=" + rememberme_token };
+ CommandArg user_agent_arg = { "-H", useragent_str };
+
+ std::string website_data;
+ if(download_to_string(url, website_data, {std::move(cookie_arg), std::move(user_agent_arg)}, use_tor) != DownloadResult::OK)
+ return SuggestionResult::NET_ERR;
+
+ QuickMediaHtmlSearch html_search;
+ int result = quickmedia_html_search_init(&html_search, website_data.c_str());
+ if(result != 0)
+ goto cleanup;
+
+ result = quickmedia_html_find_nodes_xpath(&html_search, "//a",
+ [](QuickMediaHtmlNode *node, void *userdata) {
+ auto *item_data = (BodyItems*)userdata;
+ const char *href = quickmedia_html_node_get_attribute_value(node, "href");
+ const char *title = quickmedia_html_node_get_attribute_value(node, "title");
+ if(title && href && strncmp(href, "/title/", 7) == 0) {
+ auto item = std::make_unique<BodyItem>(strip(title));
+ item->url = mangadex_url + href;
+ item_data->push_back(std::move(item));
+ }
+ }, &result_items);
+
+ BodyItemImageContext body_item_image_context;
+ body_item_image_context.body_items = &result_items;
+ body_item_image_context.index = 0;
+
+ result = quickmedia_html_find_nodes_xpath(&html_search, "//img",
+ [](QuickMediaHtmlNode *node, void *userdata) {
+ auto *item_data = (BodyItemImageContext*)userdata;
+ const char *src = quickmedia_html_node_get_attribute_value(node, "src");
+ if(src && strncmp(src, "/images/manga/", 14) == 0) {
+ if(item_data->index < item_data->body_items->size()) {
+ (*item_data->body_items)[item_data->index]->thumbnail_url = mangadex_url + src;
+ item_data->index++;
+ }
+ }
+ }, &body_item_image_context);
+
+ cleanup:
+ quickmedia_html_search_deinit(&html_search);
+ return result == 0 ? SuggestionResult::OK : SuggestionResult::ERR;
+ }
+
+ ImageResult Mangadex::get_number_of_images(const std::string &url, int &num_images) {
+ std::lock_guard<std::mutex> lock(image_urls_mutex);
+ num_images = 0;
+ ImageResult image_result = get_image_urls_for_chapter(url);
+ if(image_result != ImageResult::OK)
+ return image_result;
+
+ num_images = last_chapter_image_urls.size();
+ return ImageResult::OK;
+ }
+
+ ImageResult Mangadex::get_image_urls_for_chapter(const std::string &url) {
+ if(url == last_chapter_url)
+ return ImageResult::OK;
+
+ last_chapter_image_urls.clear();
+
+ CommandArg user_agent_arg = { "-H", useragent_str };
+ std::string manga_id = chapter_url_extract_manga_id(url);
+ std::string request_url = mangadex_url + "/api/?id=" + manga_id + "&server=null&type=chapter";
+ std::string server_response;
+ if(download_to_string(request_url, server_response, {std::move(user_agent_arg)}, use_tor) != DownloadResult::OK)
+ return ImageResult::NET_ERR;
+
+ if(server_response.empty())
+ return ImageResult::OK;
+
+ 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[0], &server_response[server_response.size()], &json_root, &json_errors)) {
+ fprintf(stderr, "Mangadex image urls json error: %s\n", json_errors.c_str());
+ return ImageResult::ERR;
+ }
+
+ Json::Value &status_json = json_root["status"];
+ if(!status_json.isString() || status_json.asString() != "OK")
+ return ImageResult::ERR;
+
+ Json::Value &chapter_hash = json_root["hash"];
+ if(!chapter_hash.isString())
+ return ImageResult::ERR;
+ const char *chapter_hash_str = chapter_hash.asCString();
+
+ Json::Value &server_json = json_root["server"];
+ std::string server;
+ if(server_json.isString())
+ server = server_json.asString();
+ else
+ server = mangadex_url + "/data/";
+
+ Json::Value &page_array = json_root["page_array"];
+ if(page_array.isArray()) {
+ for(const Json::Value &image_name : page_array) {
+ if(!image_name.isString())
+ continue;
+
+ std::string image_url = server + chapter_hash_str + "/" + image_name.asCString();
+ last_chapter_image_urls.push_back(std::move(image_url));
+ }
+ }
+
+ last_chapter_url = url;
+ if(last_chapter_image_urls.empty()) {
+ last_chapter_url.clear();
+ return ImageResult::ERR;
+ }
+ return ImageResult::OK;
+ }
+
+ ImageResult Mangadex::for_each_page_in_chapter(const std::string &chapter_url, PageCallback callback) {
+ std::vector<std::string> image_urls;
+ {
+ std::lock_guard<std::mutex> lock(image_urls_mutex);
+ ImageResult image_result = get_image_urls_for_chapter(chapter_url);
+ if(image_result != ImageResult::OK)
+ return image_result;
+
+ image_urls = last_chapter_image_urls;
+ }
+
+ for(const std::string &url : image_urls) {
+ if(!callback(url))
+ break;
+ }
+ return ImageResult::OK;
+ }
+
+ bool Mangadex::extract_id_from_url(const std::string &url, std::string &manga_id) {
+ manga_id = title_url_extract_manga_id(url);
+ return true;
+ }
+}
diff --git a/src/plugins/Manganelo.cpp b/src/plugins/Manganelo.cpp
index a97f75a..89f882c 100644
--- a/src/plugins/Manganelo.cpp
+++ b/src/plugins/Manganelo.cpp
@@ -1,4 +1,5 @@
#include "../../plugins/Manganelo.hpp"
+#include "../../include/Notification.hpp"
#include <quickmedia/HtmlSearch.h>
#include <json/reader.h>
@@ -159,4 +160,37 @@ namespace QuickMedia {
}
return ImageResult::OK;
}
+
+ bool Manganelo::extract_id_from_url(const std::string &url, std::string &manga_id) {
+ bool manganelo_website = false;
+ if(url.find("mangakakalot") != std::string::npos || url.find("manganelo") != std::string::npos)
+ manganelo_website = true;
+
+ if(manganelo_website) {
+ size_t index = url.find("manga/");
+ if(index == std::string::npos) {
+ std::string err_msg = "Url ";
+ err_msg += url;
+ err_msg += " doesn't contain manga id";
+ show_notification("Manga", err_msg, Urgency::CRITICAL);
+ return false;
+ }
+
+ 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);
+ return false;
+ }
+ 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);
+ return false;
+ }
+ }
} \ No newline at end of file