aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/QuickMedia.cpp11
-rw-r--r--src/plugins/AniList.cpp514
2 files changed, 520 insertions, 5 deletions
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index e6c642d..7f83cb9 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -15,6 +15,7 @@
#include "../plugins/Info.hpp"
#include "../plugins/HotExamples.hpp"
#include "../plugins/MyAnimeList.hpp"
+#include "../plugins/AniList.hpp"
#include "../include/Scale.hpp"
#include "../include/Program.hpp"
#include "../include/VideoPlayer.hpp"
@@ -78,7 +79,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("anilist", "anilist_logo.png"),
std::make_pair("hotexamples", nullptr),
std::make_pair("file-manager", nullptr),
std::make_pair("stdin", nullptr),
@@ -351,7 +352,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, mal, 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, anilist, 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");
@@ -1097,8 +1098,8 @@ namespace QuickMedia {
auto pipe_body = create_body(true);
pipe_body->set_items({
create_launcher_body_item("4chan", "4chan", resources_root + "icons/4chan_launcher.png"),
+ create_launcher_body_item("AniList", "anilist", resources_root + "images/anilist_logo.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"),
@@ -1212,8 +1213,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, "anilist") == 0) {
+ tabs.push_back(Tab{create_body(), std::make_unique<AniListSearchPage>(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))
diff --git a/src/plugins/AniList.cpp b/src/plugins/AniList.cpp
new file mode 100644
index 0000000..5bf069c
--- /dev/null
+++ b/src/plugins/AniList.cpp
@@ -0,0 +1,514 @@
+#include "../../plugins/AniList.hpp"
+#include "../../include/Theme.hpp"
+#include "../../include/StringUtils.hpp"
+#include "../../include/Notification.hpp"
+#include <json/writer.h>
+
+// TODO: Use get_scale() to fetch correctly sized thumbnail
+
+namespace QuickMedia {
+ static const std::string search_query_graphql = R"END(
+query ($page: Int, $perPage: Int, $search: String) {
+ Page (page: $page, perPage: $perPage) {
+ media (search: $search) {
+ id
+ type
+ format
+ status (version: 2)
+ startDate {
+ year
+ month
+ day
+ }
+ endDate {
+ year
+ month
+ day
+ }
+ averageScore
+ episodes
+ chapters
+ genres
+ coverImage {
+ medium
+ }
+ title {
+ romaji
+ }
+ synonyms
+ }
+ }
+}
+ )END";
+
+ // TODO: description asHtml seems to be broken, so for now we manually remove html from the description text.
+ // Remove that part when anilist api is fixed.
+ static const std::string details_query_graphql = R"END(
+query ($id: Int) {
+ Media (id: $id) {
+ type
+ format
+ status (version: 2)
+ startDate {
+ year
+ month
+ day
+ }
+ endDate {
+ year
+ month
+ day
+ }
+ averageScore
+ episodes
+ chapters
+ genres
+ description (asHtml: false)
+ coverImage {
+ large
+ }
+ title {
+ romaji
+ }
+ synonyms
+ }
+}
+ )END";
+
+ enum class AniListMediaType {
+ ANIME,
+ MANGA
+ };
+
+ static const char *month_names[] = { "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec" };
+ static bool in_range(int value, int min, int max) {
+ return value >= min && value <= max;
+ }
+
+ static std::string fuzzy_date_to_str(const Json::Value &fuzzy_date_json) {
+ std::string result;
+ if(!fuzzy_date_json.isObject())
+ return result;
+
+ const Json::Value &year_json = fuzzy_date_json["year"];
+ const Json::Value &month_json = fuzzy_date_json["month"];
+ const Json::Value &day_json = fuzzy_date_json["day"];
+ if(!year_json.isInt())
+ return result;
+
+ if(!month_json.isInt() || !in_range(month_json.asInt(), 1, 12)) {
+ result = std::to_string(year_json.asInt());
+ return result;
+ }
+
+ result += month_names[month_json.asInt() - 1];
+ if(day_json.isInt()) {
+ result += " ";
+ result += std::to_string(day_json.asInt());
+ }
+ result += ", ";
+ result += std::to_string(year_json.asInt());
+ return result;
+ }
+
+ static const char* media_status_to_readable(const char *media_status, AniListMediaType media_type) {
+ if(strcmp(media_status, "FINISHED") == 0)
+ return media_type == AniListMediaType::ANIME ? "Finished airing" : "Finished";
+ if(strcmp(media_status, "RELEASING") == 0)
+ return media_type == AniListMediaType::ANIME ? "Publishing" : "Airing";
+ if(strcmp(media_status, "NOT_YET_RELEASED") == 0)
+ return "Not yet released";
+ if(strcmp(media_status, "CANCELLED") == 0)
+ return "Cancelled";
+ if(strcmp(media_status, "HIATUS") == 0)
+ return "Hiatus";
+ return media_status;
+ }
+
+ static const char* media_format_to_readable(const char *media_format) {
+ if(strcmp(media_format, "TV") == 0)
+ return "TV";
+ if(strcmp(media_format, "TV_SHORT") == 0)
+ return "TV short";
+ if(strcmp(media_format, "MOVIE") == 0)
+ return "Movie";
+ if(strcmp(media_format, "SPECIAL") == 0)
+ return "Special";
+ if(strcmp(media_format, "OVA") == 0)
+ return "OVA";
+ if(strcmp(media_format, "ONA") == 0)
+ return "ONA";
+ if(strcmp(media_format, "MUSIC") == 0)
+ return "Music";
+ if(strcmp(media_format, "MANGA") == 0)
+ return "Manga";
+ if(strcmp(media_format, "NOVEL") == 0)
+ return "Novel";
+ if(strcmp(media_format, "ONE_SHOT") == 0)
+ return "One shot";
+ return media_format;
+ }
+
+ static std::string json_string_array_to_string(const Json::Value &json_arr) {
+ std::string result;
+ if(!json_arr.isArray())
+ return result;
+
+ for(const Json::Value &json_item : json_arr) {
+ if(!json_item.isString())
+ continue;
+ if(!result.empty())
+ result += ", ";
+ result += json_item.asString();
+ }
+
+ return result;
+ }
+
+ static void description_remove_html(std::string &description) {
+ string_replace_all(description, "<i>", "");
+ string_replace_all(description, "</i>", "");
+ string_replace_all(description, "<b>", "");
+ string_replace_all(description, "</b>", "");
+ string_replace_all(description, "<br>", "");
+ html_unescape_sequences(description);
+ }
+
+ enum class ThumbnailSize {
+ MEDIUM,
+ LARGE
+ };
+
+ static const char* thumbnail_size_to_cover_image_type(ThumbnailSize thumbnail_size) {
+ switch(thumbnail_size) {
+ case ThumbnailSize::MEDIUM: return "medium";
+ case ThumbnailSize::LARGE: return "large";
+ }
+ return "";
+ }
+
+ // TODO: Somehow get the correct thumbnail size?
+ static sf::Vector2i thumbnail_size_get_prediced_size(ThumbnailSize thumbnail_size) {
+ switch(thumbnail_size) {
+ case ThumbnailSize::MEDIUM: return {100, 158};
+ case ThumbnailSize::LARGE: return {215, 304};
+ }
+ return {0, 0};
+ }
+
+ // Returns nullptr on error
+ static std::shared_ptr<BodyItem> media_json_to_body_item(const Json::Value &media_json, ThumbnailSize thumbnail_size, AniListMediaType &media_type) {
+ if(!media_json.isObject())
+ return nullptr;
+
+ const Json::Value &type_json = media_json["type"];
+ const Json::Value &title_json = media_json["title"];
+ if(!type_json.isString() || !title_json.isObject())
+ return nullptr;
+
+ const Json::Value &id_json = media_json["id"];
+ const Json::Value &cover_image_json = media_json["coverImage"];
+ const Json::Value &synonyms_json = media_json["synonyms"];
+ const Json::Value &start_date_json = media_json["startDate"];
+ const Json::Value &end_date_json = media_json["endDate"];
+ const Json::Value &average_score_json = media_json["averageScore"];
+ const Json::Value &format_json = media_json["format"];
+ const Json::Value &status_json = media_json["status"];
+ const Json::Value &episodes_json = media_json["episodes"];
+ const Json::Value &chapters_json = media_json["chapters"];
+ const Json::Value &genres_json = media_json["genres"];
+ const Json::Value &description_json = media_json["description"];
+
+ const Json::Value &romaji_title_json = title_json["romaji"];
+ if(!romaji_title_json.isString())
+ return nullptr;
+
+ if(strcmp(type_json.asCString(), "ANIME") == 0)
+ media_type = AniListMediaType::ANIME;
+ else if(strcmp(type_json.asCString(), "MANGA") == 0)
+ media_type = AniListMediaType::MANGA;
+ else
+ return nullptr;
+
+ std::string title = romaji_title_json.asString();
+ if(format_json.isString()) {
+ title += " (";
+ title += media_format_to_readable(format_json.asCString());
+ title += ")";
+ }
+
+ std::string description;
+ if(synonyms_json.isArray() && synonyms_json.size() > 0)
+ description += "Synonyms: " + json_string_array_to_string(synonyms_json);
+
+ std::string start_date_str = fuzzy_date_to_str(start_date_json);
+ std::string end_date_str = fuzzy_date_to_str(end_date_json);
+ const char *aired_prefix = nullptr;
+ if(media_type == AniListMediaType::ANIME)
+ aired_prefix = "Aired";
+ else if(media_type == AniListMediaType::MANGA)
+ aired_prefix = "Published";
+
+ if(!start_date_str.empty()) {
+ if(!description.empty())
+ description += '\n';
+ description += aired_prefix;
+ description += ": " + std::move(start_date_str);
+
+ if(end_date_str.empty())
+ end_date_str = "?";
+
+ description += " to " + std::move(end_date_str);
+ }
+
+ if(average_score_json.isInt()) {
+ if(!description.empty())
+ description += '\n';
+ description += "Score: ";
+
+ char buffer[32];
+ int len = snprintf(buffer, sizeof(buffer), "%.2f", (double)average_score_json.asInt() / 10.0);
+ if(len > 0)
+ description.append(buffer, len);
+ }
+
+ if(episodes_json.isInt()) {
+ if(!description.empty())
+ description += '\n';
+ description += "Episodes: " + std::to_string(episodes_json.asInt());
+ }
+
+ if(chapters_json.isInt()) {
+ if(!description.empty())
+ description += '\n';
+ description += "Chapters: " + std::to_string(chapters_json.asInt());
+ }
+
+ if(status_json.isString()) {
+ if(!description.empty())
+ description += '\n';
+ description += "Status: ";
+ description += media_status_to_readable(status_json.asCString(), media_type);
+ }
+
+ if(genres_json.isArray() && genres_json.size() > 0) {
+ if(!description.empty())
+ description += '\n';
+ description += "Genres: " + json_string_array_to_string(genres_json);
+ }
+
+ if(description_json.isString()) {
+ if(!description.empty())
+ description += "\n\n";
+ description += "Synopsis: ";
+
+ std::string synopsis = description_json.asString();
+ description_remove_html(synopsis);
+ description += std::move(synopsis);
+ }
+
+ auto body_item = BodyItem::create("");
+ if(id_json.isInt())
+ body_item->url = std::to_string(id_json.asInt());
+ body_item->set_author(std::move(title));
+ if(!description.empty()) {
+ body_item->set_description(std::move(description));
+ body_item->set_description_color(get_current_theme().faded_text_color);
+ }
+ if(cover_image_json.isObject()) {
+ const Json::Value &cover_img_sized_json = cover_image_json[thumbnail_size_to_cover_image_type(thumbnail_size)];
+ if(cover_img_sized_json.isString())
+ body_item->thumbnail_url = cover_img_sized_json.asString();
+ }
+ body_item->thumbnail_size = thumbnail_size_get_prediced_size(thumbnail_size);
+
+ return body_item;
+ }
+
+ SearchResult AniListSearchPage::search_page(const std::string &str, int page, BodyItems &result_items) {
+ Json::Value variables_json(Json::objectValue);
+ variables_json["page"] = page;
+ variables_json["perPage"] = 20;
+ variables_json["search"] = str;
+
+ Json::Value request_json(Json::objectValue);
+ request_json["query"] = search_query_graphql;
+ request_json["variables"] = std::move(variables_json);
+
+ Json::StreamWriterBuilder json_builder;
+ json_builder["commentStyle"] = "None";
+ json_builder["indentation"] = "";
+
+ std::vector<CommandArg> additional_args = {
+ { "-X", "POST" },
+ { "-H", "content-type: application/json" },
+ { "--data-binary", Json::writeString(json_builder, request_json) }
+ };
+
+ Json::Value json_root;
+ std::string err_msg;
+ DownloadResult download_result = download_json(json_root, "https://graphql.anilist.co", std::move(additional_args), true, &err_msg);
+ if(download_result != DownloadResult::OK) return download_result_to_search_result(download_result);
+
+ if(!json_root.isObject())
+ return SearchResult::ERR;
+
+ const Json::Value &errors_json = json_root["errors"];
+ if(errors_json.isArray() && errors_json.size() > 0) {
+ const Json::Value &error_json = errors_json[0];
+ if(error_json.isObject()) {
+ const Json::Value &error_message_json = error_json["message"];
+ if(error_message_json.isString()) {
+ show_notification("QuickMedia", "AniList search failed, error: " + error_message_json.asString(), Urgency::CRITICAL);
+ return SearchResult::ERR;
+ }
+ }
+ }
+
+ const Json::Value &data_json = json_root["data"];
+ if(!data_json.isObject())
+ return SearchResult::ERR;
+
+ const Json::Value &page_json = data_json["Page"];
+ if(!page_json.isObject())
+ return SearchResult::ERR;
+
+ const Json::Value &media_list_json = page_json["media"];
+ if(!media_list_json.isArray())
+ return SearchResult::ERR;
+
+ BodyItems anime_items;
+ BodyItems manga_items;
+ for(const Json::Value &media_json : media_list_json) {
+ AniListMediaType media_type;
+ auto body_item = media_json_to_body_item(media_json, ThumbnailSize::MEDIUM, media_type);
+ if(!body_item)
+ continue;
+
+ if(media_type == AniListMediaType::ANIME)
+ anime_items.push_back(std::move(body_item));
+ else if(media_type == AniListMediaType::MANGA)
+ manga_items.push_back(std::move(body_item));
+ }
+
+ if(anime_items.empty() && manga_items.empty())
+ return SearchResult::OK;
+
+ 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;
+ }
+
+ SearchResult AniListSearchPage::search(const std::string &str, BodyItems &result_items) {
+ return search_page(str, 1, result_items);
+ }
+
+ PluginResult AniListSearchPage::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<AniListRelatedPage>(program, url), nullptr });
+ result_tabs.push_back({ create_body(), std::make_unique<AniListDetailsPage>(program, url), nullptr });
+ result_tabs.push_back({ create_body(), std::make_unique<AniListRecommendationsPage>(program, url), nullptr });
+ return PluginResult::OK;
+ }
+
+ PluginResult AniListSearchPage::get_page(const std::string &str, int page, BodyItems &result_items) {
+ return search_result_to_plugin_result(search_page(str, 1 + page, result_items));
+ }
+
+ PluginResult AniListRelatedPage::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<AniListRelatedPage>(program, url), nullptr });
+ result_tabs.push_back({ create_body(), std::make_unique<AniListDetailsPage>(program, url), nullptr });
+ result_tabs.push_back({ create_body(), std::make_unique<AniListRecommendationsPage>(program, url), nullptr });
+ return PluginResult::OK;
+ }
+
+ PluginResult AniListRelatedPage::lazy_fetch(BodyItems &result_items) {
+ return PluginResult::OK;
+ }
+
+ PluginResult AniListDetailsPage::submit(const std::string&, const std::string&, std::vector<Tab>&) {
+ return PluginResult::OK;
+ }
+
+ PluginResult AniListDetailsPage::lazy_fetch(BodyItems &result_items) {
+ Json::Value variables_json(Json::objectValue);
+ variables_json["id"] = atoi(id.c_str());
+
+ Json::Value request_json(Json::objectValue);
+ request_json["query"] = details_query_graphql;
+ request_json["variables"] = std::move(variables_json);
+
+ Json::StreamWriterBuilder json_builder;
+ json_builder["commentStyle"] = "None";
+ json_builder["indentation"] = "";
+
+ std::vector<CommandArg> additional_args = {
+ { "-X", "POST" },
+ { "-H", "content-type: application/json" },
+ { "--data-binary", Json::writeString(json_builder, request_json) }
+ };
+
+ Json::Value json_root;
+ std::string err_msg;
+ DownloadResult download_result = download_json(json_root, "https://graphql.anilist.co", std::move(additional_args), true, &err_msg);
+ if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result);
+
+ if(!json_root.isObject())
+ return PluginResult::ERR;
+
+ const Json::Value &errors_json = json_root["errors"];
+ if(errors_json.isArray() && errors_json.size() > 0) {
+ const Json::Value &error_json = errors_json[0];
+ if(error_json.isObject()) {
+ const Json::Value &error_message_json = error_json["message"];
+ if(error_message_json.isString()) {
+ show_notification("QuickMedia", "AniList search failed, error: " + error_message_json.asString(), Urgency::CRITICAL);
+ return PluginResult::ERR;
+ }
+ }
+ }
+
+ const Json::Value &data_json = json_root["data"];
+ if(!data_json.isObject())
+ return PluginResult::ERR;
+
+ const Json::Value &media_json = data_json["Media"];
+ if(!media_json.isObject())
+ return PluginResult::ERR;
+
+ AniListMediaType media_type;
+ auto body_item = media_json_to_body_item(media_json, ThumbnailSize::LARGE, media_type);
+ if(!body_item)
+ return PluginResult::ERR;
+
+ result_items.push_back(std::move(body_item));
+ return PluginResult::OK;
+ }
+
+ PluginResult AniListRecommendationsPage::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<AniListRelatedPage>(program, url), nullptr });
+ result_tabs.push_back({ create_body(), std::make_unique<AniListDetailsPage>(program, url), nullptr });
+ result_tabs.push_back({ create_body(), std::make_unique<AniListRecommendationsPage>(program, url), nullptr });
+ return PluginResult::OK;
+ }
+
+ PluginResult AniListRecommendationsPage::lazy_fetch(BodyItems &result_items) {
+ return PluginResult::OK;
+ }
+} \ No newline at end of file