aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/AsyncImageLoader.cpp5
-rw-r--r--src/Config.cpp4
-rw-r--r--src/Entry.cpp4
-rw-r--r--src/FileAnalyzer.cpp2
-rw-r--r--src/ImageViewer.cpp25
-rw-r--r--src/QuickMedia.cpp170
-rw-r--r--src/RoundedRectangle.cpp2
-rw-r--r--src/Storage.cpp47
-rw-r--r--src/Text.cpp4
-rw-r--r--src/Utils.cpp15
-rw-r--r--src/VideoPlayer.cpp43
-rw-r--r--src/main.cpp2
-rw-r--r--src/plugins/DramaCool.cpp190
-rw-r--r--src/plugins/LocalAnime.cpp63
-rw-r--r--src/plugins/LocalManga.cpp19
-rw-r--r--src/plugins/Matrix.cpp114
-rw-r--r--src/plugins/Youtube.cpp97
-rw-r--r--src/plugins/utils/EpisodeNameParser.cpp7
-rw-r--r--src/plugins/utils/aes.c571
-rw-r--r--src/plugins/utils/aes.h91
20 files changed, 1307 insertions, 168 deletions
diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp
index 1118ee6..a6706f5 100644
--- a/src/AsyncImageLoader.cpp
+++ b/src/AsyncImageLoader.cpp
@@ -26,7 +26,8 @@
#pragma GCC diagnostic pop
namespace QuickMedia {
- static bool ffmpeg_image_to_png(const Path &thumbnail_path, const Path &destination_path) {
+ bool ffmpeg_convert_image_format(const Path &thumbnail_path, const Path &destination_path) {
+ // TODO: Dont output as jpg, ffmpeg jpg is very slow
const char *args[] = { "ffmpeg", "-y", "-v", "quiet", "-i", thumbnail_path.data.c_str(), "--", destination_path.data.c_str(), nullptr};
return exec_program(args, nullptr, nullptr) == 0;
}
@@ -37,7 +38,7 @@ namespace QuickMedia {
if(content_type == ContentType::IMAGE_WEBP || content_type == ContentType::IMAGE_AVIF) {
Path result_path_tmp = thumbnail_path_resized;
result_path_tmp.append(".tmp.png");
- if(!ffmpeg_image_to_png(thumbnail_path, result_path_tmp))
+ if(!ffmpeg_convert_image_format(thumbnail_path, result_path_tmp))
return false;
input_path = std::move(result_path_tmp);
}
diff --git a/src/Config.cpp b/src/Config.cpp
index 5d16083..64d5019 100644
--- a/src/Config.cpp
+++ b/src/Config.cpp
@@ -275,6 +275,7 @@ namespace QuickMedia {
get_json_value(local_anime_json, "sort_by_name", config->local_anime.sort_by_name);
get_json_value(local_anime_json, "sort_episodes_by_name", config->local_anime.sort_episodes_by_name);
get_json_value(local_anime_json, "auto_group_episodes", config->local_anime.auto_group_episodes);
+ get_json_value(local_anime_json, "recursive", config->local_anime.recursive);
}
const Json::Value &youtube_json = json_root["youtube"];
@@ -405,6 +406,7 @@ namespace QuickMedia {
get_json_value(json_root, "use_system_fonts", config->use_system_fonts);
get_json_value(json_root, "use_system_mpv_config", config->use_system_mpv_config);
+ get_json_value(json_root, "system_mpv_profile", config->system_mpv_profile);
get_json_value(json_root, "enable_shaders", config->enable_shaders);
get_json_value(json_root, "theme", config->theme);
get_json_value(json_root, "scale", config->scale);
@@ -413,7 +415,7 @@ namespace QuickMedia {
get_json_value(json_root, "low_latency_mode", config->low_latency_mode);
}
- const Config& get_config() {
+ Config& get_config() {
init_config();
return *config;
}
diff --git a/src/Entry.cpp b/src/Entry.cpp
index 5416724..8aa2bba 100644
--- a/src/Entry.cpp
+++ b/src/Entry.cpp
@@ -107,6 +107,10 @@ namespace QuickMedia {
return text.getCaretIndex();
}
+ void Entry::set_caret_index(int index) {
+ text.setCaretIndex(index);
+ }
+
void Entry::set_position(const mgl::vec2f &pos) {
background.set_position(pos);
text.set_position(pos + mgl::vec2f(background_margin_horizontal * padding_scale, background_margin_vertical * padding_scale - (float)text.get_character_size() * 0.3f).floor());
diff --git a/src/FileAnalyzer.cpp b/src/FileAnalyzer.cpp
index 4deb8c3..1d24c5f 100644
--- a/src/FileAnalyzer.cpp
+++ b/src/FileAnalyzer.cpp
@@ -157,6 +157,7 @@ namespace QuickMedia {
char size_arg_str[512];
snprintf(size_arg_str, sizeof(size_arg_str), "scale=%d:%d:force_original_aspect_ratio=decrease", width, height);
+ // TODO: Dont output as jpg, ffmpeg jpg is very slow
const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-ss", seconds_str, "-i", file.get_filepath().c_str(), "-frames:v", "1", "-vf", size_arg_str, "--", destination_path_tmp.data.c_str(), nullptr };
if(exec_program(program_args, nullptr, nullptr, allowed_exit_status, 2) != 0) {
fprintf(stderr, "Failed to execute ffmpeg, maybe its not installed?\n");
@@ -168,6 +169,7 @@ namespace QuickMedia {
if(fallback_first_frame && get_file_type(destination_path_tmp) == FileType::FILE_NOT_FOUND)
return video_get_frame(file, destination_path, width, height, 0, false);
} else {
+ // TODO: Dont output as jpg, ffmpeg jpg is very slow
const char *program_args[] = { "ffmpeg", "-y", "-v", "quiet", "-ss", seconds_str, "-i", file.get_filepath().c_str(), "-frames:v", "1", "--", destination_path_tmp.data.c_str(), nullptr };
if(exec_program(program_args, nullptr, nullptr, allowed_exit_status, 2) != 0) {
fprintf(stderr, "Failed to execute ffmpeg, maybe its not installed?\n");
diff --git a/src/ImageViewer.cpp b/src/ImageViewer.cpp
index fb9912c..454f34b 100644
--- a/src/ImageViewer.cpp
+++ b/src/ImageViewer.cpp
@@ -296,6 +296,9 @@ namespace QuickMedia {
if(event.key.code == mgl::Keyboard::F)
*fit_image_to_window = !*fit_image_to_window;
+
+ if(event.key.code == mgl::Keyboard::B)
+ show_progress_bar = !show_progress_bar;
} else if(event.type == mgl::Event::KeyReleased) {
if(is_key_scroll_up(event.key))
up_pressed = false;
@@ -426,16 +429,18 @@ namespace QuickMedia {
const float font_height = page_text_character_size + 8.0f;
const float background_height = font_height + 6.0f;
- mgl::Rectangle page_text_background(mgl::vec2f(window_size.x, background_height));
- mgl::Color text_background_color = get_theme().shade_color;
- text_background_color.a = 225;
- page_text_background.set_color(text_background_color);
- page_text_background.set_position(mgl::vec2f(0.0f, window_size.y - background_height));
- window->draw(page_text_background);
-
- auto page_text_bounds = page_text.get_bounds();
- page_text.set_position(mgl::vec2f(floor(window_size.x * 0.5f - page_text_bounds.size.x * 0.5f), floor(window_size.y - background_height * 0.5f - font_height * 0.55f)));
- window->draw(page_text);
+ if(show_progress_bar) {
+ mgl::Rectangle page_text_background(mgl::vec2f(window_size.x, background_height));
+ mgl::Color text_background_color = get_theme().shade_color;
+ text_background_color.a = 225;
+ page_text_background.set_color(text_background_color);
+ page_text_background.set_position(mgl::vec2f(0.0f, window_size.y - background_height));
+ window->draw(page_text_background);
+
+ auto page_text_bounds = page_text.get_bounds();
+ page_text.set_position(mgl::vec2f(floor(window_size.x * 0.5f - page_text_bounds.size.x * 0.5f), floor(window_size.y - background_height * 0.5f - font_height * 0.55f)));
+ window->draw(page_text);
+ }
// Free pages that are not visible on the screen
int i = 0;
diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp
index 648f231..d702fc9 100644
--- a/src/QuickMedia.cpp
+++ b/src/QuickMedia.cpp
@@ -339,7 +339,7 @@ namespace QuickMedia {
}
static void usage() {
- fprintf(stderr, "usage: quickmedia [plugin|url] [--no-video] [--upscale-images] [--upscale-images-always] [--dir <directory>] [--instance <instance>] [-e <window>] [--video-max-height <height>]\n");
+ fprintf(stderr, "usage: quickmedia [plugin|url] [--no-video] [--upscale-images] [--upscale-images-always] [--dir <directory>] [--instance <instance>] [-e <window>] [--video-max-height <height>] [--theme <theme>]\n");
fprintf(stderr, "OPTIONS:\n");
fprintf(stderr, " plugin|url The plugin to use. Should be either launcher, 4chan, manga, manganelo, manganelos, mangatown, mangakatana, mangadex, onimanga, local-manga, local-anime, youtube, peertube, lbry, soundcloud, nyaa.si, matrix, saucenao, hotexamples, anilist, dramacool, file-manager, stdin, pornhub, spankbang, xvideos or xhamster. This can also be a youtube url, youtube channel url or a 4chan thread url\n");
fprintf(stderr, " --no-video Only play audio when playing a video. Disabled by default\n");
@@ -349,6 +349,7 @@ namespace QuickMedia {
fprintf(stderr, " --instance <instance> The instance to use for peertube\n");
fprintf(stderr, " -e <window-id> Embed QuickMedia into another window\n");
fprintf(stderr, " --video-max-height <height> Media plugins will try to select a video source that is this size or smaller\n");
+ fprintf(stderr, " --theme The theme to use. If this is specified then it overrides the theme selected in the QuickMedia config file. Optional.\n");
fprintf(stderr, "EXAMPLES:\n");
fprintf(stderr, " quickmedia\n");
fprintf(stderr, " quickmedia --upscale-images-always manganelo\n");
@@ -409,6 +410,7 @@ namespace QuickMedia {
std::string program_path = Path(argv[0]).parent().data;
std::string instance;
std::string download_filename;
+ std::string theme;
bool no_dialog = false;
for(int i = 1; i < argc; ++i) {
@@ -523,6 +525,15 @@ namespace QuickMedia {
}
} else if(strcmp(argv[i], "--no-dialog") == 0) {
no_dialog = true;
+ } else if(strcmp(argv[i], "--theme") == 0) {
+ if(i < argc - 1) {
+ theme = argv[i + 1];
+ ++i;
+ } else {
+ fprintf(stderr, "Missing theme after --theme argument\n");
+ usage();
+ return -1;
+ }
} else if(strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
usage();
return 0;
@@ -602,7 +613,7 @@ namespace QuickMedia {
};
no_video = force_no_video;
- init(parent_window, program_path, no_dialog);
+ init(parent_window, program_path, no_dialog, theme);
if(strcmp(plugin_name, "download") == 0) {
if(!url) {
@@ -687,7 +698,7 @@ namespace QuickMedia {
return focused_monitor_center;
}
- void Program::init(mgl::WindowHandle parent_window, std::string &program_path, bool no_dialog) {
+ void Program::init(mgl::WindowHandle parent_window, std::string &program_path, bool no_dialog, const std::string &theme) {
disp = XOpenDisplay(NULL);
if (!disp) {
show_notification("QuickMedia", "Failed to open display to X11 server", Urgency::CRITICAL);
@@ -699,6 +710,9 @@ namespace QuickMedia {
// Initialize config and theme early to prevent possible race condition on initialize
get_config();
+ if(!theme.empty())
+ get_config().theme = theme;
+
get_theme();
mgl::vec2i monitor_size;
@@ -953,7 +967,7 @@ namespace QuickMedia {
string_split(urls_str, ',', [&arrays](const char *str, size_t size) {
std::string url(str, size);
url = strip(url);
- if(!url.empty() && (arrays.empty() || arrays.back() != url))
+ if(url.find(".com") != std::string::npos && (arrays.empty() || arrays.back() != url))
arrays.push_back(std::move(url));
return true;
});
@@ -1095,6 +1109,8 @@ namespace QuickMedia {
if(!youtube_dl_name)
return false;
+ const int video_max_height = video_get_max_height();
+
std::string ytdl_format;
if(no_video)
ytdl_format = "(bestaudio/best)";
@@ -1790,7 +1806,7 @@ namespace QuickMedia {
// TODO: Remove this once manga history file has been in use for a few months and is filled with history
time_t now = time(NULL);
- for_files_in_dir_sort_last_modified(content_storage_dir, [&](const Path &filepath, FileType) {
+ for_files_in_dir_sort_last_modified(content_storage_dir, [&](const Path &filepath, FileType, time_t last_modified_seconds) {
// This can happen when QuickMedia crashes/is killed while writing to storage.
// In that case, the storage wont be corrupt but there will be .tmp files.
// TODO: Remove these .tmp files if they exist during startup
@@ -1812,12 +1828,12 @@ namespace QuickMedia {
if(!manga_name.isString())
return true;
- time_t last_modified_time = 0;
- file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_time);
+ if(last_modified_seconds == 0)
+ file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds);
// TODO: Add thumbnail
auto body_item = BodyItem::create(manga_name.asString());
- body_item->set_description("Last read " + seconds_to_relative_time_str(now - last_modified_time));
+ body_item->set_description("Last read " + seconds_to_relative_time_str(now - last_modified_seconds));
body_item->set_description_color(get_theme().faded_text_color);
auto thumbnail_it = manga_id_to_thumbnail_url_map.find(manga_id);
@@ -3086,7 +3102,10 @@ namespace QuickMedia {
}
static bool url_should_download_with_youtube_dl(const std::string &url) {
- return url.find("pornhub.com") != std::string::npos
+ return url.find("youtube.com") != std::string::npos
+ || url.find("youtu.be") != std::string::npos
+ || url.find("googlevideo.com") != std::string::npos
+ || url.find("pornhub.com") != std::string::npos
|| url.find("xhamster.com") != std::string::npos
|| url.find("spankbang.com") != std::string::npos
// TODO: Remove when youtube-dl is no longer required to download soundcloud music
@@ -3330,18 +3349,21 @@ namespace QuickMedia {
std::string audio_url;
bool has_embedded_audio = true;
+ bool update_duration_retry = false;
auto update_video_duration_handler = [&]() {
if(!video_player)
return;
- if(update_duration) {
+ if(update_duration || update_duration_retry) {
update_duration = false;
successfully_fetched_video_duration = false;
double file_duration = 0.0;
video_player->get_duration(&file_duration);
video_info.duration = std::max(video_info.duration, file_duration);
- if(video_info.duration > 0.001)
+ if(video_info.duration > 0.001) {
+ update_duration_retry = false;
successfully_fetched_video_duration = true;
+ }
}
};
@@ -3361,6 +3383,9 @@ namespace QuickMedia {
update_time_pos = true;
}
+ if(update_duration || (update_duration_retry && update_time_pos))
+ update_video_duration_handler();
+
if(update_time_pos) {
update_time_pos = false;
const double prev_video_time_pos = video_time_pos;
@@ -3382,8 +3407,6 @@ namespace QuickMedia {
}
}
}
-
- update_video_duration_handler();
};
auto load_video_error_check = [&](std::string start_time = "", bool reuse_media_source = false) mutable {
@@ -3510,6 +3533,7 @@ namespace QuickMedia {
startup_args.parent_window = window.get_system_handle();
startup_args.no_video = is_audio_only;
startup_args.use_system_mpv_config = get_config().use_system_mpv_config || video_page->is_local();
+ startup_args.system_mpv_profile = get_config().system_mpv_profile;
startup_args.use_system_input_config = video_page->is_local();
startup_args.keep_open = is_matrix && !is_youtube;
startup_args.resume = false;
@@ -3589,6 +3613,7 @@ namespace QuickMedia {
//video_player->set_paused(false);
} else if(strcmp(event_name, "start-file") == 0) {
update_duration = true;
+ update_duration_retry = true;
added_recommendations = false;
time_watched_timer.restart();
video_loaded = true;
@@ -3621,7 +3646,7 @@ namespace QuickMedia {
mgl::Clock cursor_hide_timer;
auto save_video_url_to_clipboard = [this, video_page]() {
- std::string url = video_page->get_download_url(video_get_max_height());
+ std::string url = video_page->get_download_url_for_clipboard(video_get_max_height());
if(video_url_supports_timestamp(url)) {
double time_in_file = 0.0;
if(video_player && (video_player->get_time_in_file(&time_in_file) != VideoPlayer::Error::OK))
@@ -4230,6 +4255,15 @@ namespace QuickMedia {
}
}
+ {
+ FileAnalyzer file_analyzer;
+ if(file_analyzer.load_file(image_filepath_tmp.data.c_str(), false) && file_analyzer.get_content_type() == ContentType::IMAGE_WEBP) {
+ Path new_filepath = image_filepath_tmp.data + ".png";
+ if(ffmpeg_convert_image_format(image_filepath_tmp, new_filepath))
+ image_filepath_tmp = std::move(new_filepath);
+ }
+ }
+
bool rename_immediately = true;
if(upscale_image_action == UpscaleImageAction::LOW_RESOLUTION) {
int screen_width, screen_height;
@@ -4476,6 +4510,9 @@ namespace QuickMedia {
} else if(event.key.code == mgl::Keyboard::F) {
fit_image_to_window = !fit_image_to_window;
redraw = true;
+ } else if(event.key.code == mgl::Keyboard::B) {
+ show_manga_bottom_bar = !show_manga_bottom_bar;
+ redraw = true;
}
}
}
@@ -4500,7 +4537,7 @@ namespace QuickMedia {
}
const float font_height = chapter_text_character_size + 8.0f;
- const float bottom_panel_height = font_height + 6.0f;
+ const float bottom_panel_height = show_manga_bottom_bar ? font_height + 6.0f : 0.0f;
mgl::vec2f content_size;
content_size.x = window_size.x;
@@ -4539,13 +4576,15 @@ namespace QuickMedia {
window.draw(image);
}
- chapter_text_background.set_size(mgl::vec2f(window_size.x, bottom_panel_height));
- chapter_text_background.set_position(mgl::vec2f(0.0f, std::floor(window_size.y - bottom_panel_height)));
- window.draw(chapter_text_background);
+ if(show_manga_bottom_bar) {
+ chapter_text_background.set_size(mgl::vec2f(window_size.x, bottom_panel_height));
+ chapter_text_background.set_position(mgl::vec2f(0.0f, std::floor(window_size.y - bottom_panel_height)));
+ window.draw(chapter_text_background);
- auto text_bounds = chapter_text.get_bounds();
- chapter_text.set_position(vec2f_floor(window_size.x * 0.5f - text_bounds.size.x * 0.5f, window_size.y - bottom_panel_height * 0.5f - font_height * 0.55f));
- window.draw(chapter_text);
+ auto text_bounds = chapter_text.get_bounds();
+ chapter_text.set_position(vec2f_floor(window_size.x * 0.5f - text_bounds.size.x * 0.5f, window_size.y - bottom_panel_height * 0.5f - font_height * 0.55f));
+ window.draw(chapter_text);
+ }
window.display();
} else {
@@ -6280,7 +6319,9 @@ namespace QuickMedia {
"/me [text]: Send a message of type \"m.emote\".\n"
"/react [text]: React to the selected message (also works if you are replying to a message).\n"
"/id: Show the room id.\n"
- "/encrypt [text]: Send a message encrypted with gpg. gpg needs to be installed to do this. Uses the gpg key specified by the user id in your config variable \"matrix.gpg_user_id\".";
+ "/whoami: Show your user id.\n"
+ "/encrypt [text]: Send a message encrypted with gpg. gpg needs to be installed to do this. Uses the gpg key specified by the user id in your config variable \"matrix.gpg_user_id\".\n"
+ "/bot [text]: Send a message as a bot would.";
message->timestamp = time(nullptr) * 1000; // TODO: What if the user has broken local time?
matrix->append_system_message(current_room, std::move(message));
@@ -6300,9 +6341,24 @@ namespace QuickMedia {
chat_state = ChatState::NAVIGATING;
matrix->get_room_extra_data(current_room).editing_message_id.clear();
return true;
+ } else if(text == "/whoami") {
+ auto message = std::make_shared<Message>();
+ message->type = MessageType::SYSTEM;
+ message->user = me;
+ message->body = me->user_id;
+ message->timestamp = time(nullptr) * 1000; // TODO: What if the user has broken local time?
+ matrix->append_system_message(current_room, std::move(message));
+
+ chat_input.set_editable(false);
+ chat_state = ChatState::NAVIGATING;
+ matrix->get_room_extra_data(current_room).editing_message_id.clear();
+ return true;
} else if(strncmp(text.c_str(), "/me ", 4) == 0) {
msgtype = "m.emote";
text.erase(text.begin(), text.begin() + 4);
+ } else if(strncmp(text.c_str(), "/bot ", 5) == 0) {
+ msgtype = "m.notice";
+ text.erase(text.begin(), text.begin() + 5);
} else if(strncmp(text.c_str(), "/react ", 7) == 0) {
msgtype = "m.reaction";
text.erase(text.begin(), text.begin() + 7);
@@ -7112,6 +7168,30 @@ namespace QuickMedia {
}
};
+ auto start_typing = [&] {
+ RoomExtraData &room_extra_data = matrix->get_room_extra_data(current_room);
+ frame_skip_text_entry = true;
+ chat_input.set_editable(true);
+ if(get_config().matrix.clear_message_on_escape || !room_extra_data.editing_message_id.empty()) {
+ chat_input.set_text("");
+ room_extra_data.editing_message_id.clear();
+ }
+ chat_state = ChatState::TYPING_MESSAGE;
+ };
+
+ auto start_replying = [&](std::shared_ptr<BodyItem> selected_body_item) {
+ RoomExtraData &room_extra_data = matrix->get_room_extra_data(current_room);
+ chat_state = ChatState::REPLYING;
+ currently_operating_on_item = selected_body_item;
+ chat_input.set_editable(true);
+ if(get_config().matrix.clear_message_on_escape || !room_extra_data.editing_message_id.empty()) {
+ chat_input.set_text("");
+ room_extra_data.editing_message_id.clear();
+ }
+ replying_to_text.set_string("Replying to:");
+ frame_skip_text_entry = true;
+ };
+
for(size_t i = 0; i < tabs.size(); ++i) {
tabs[i].body->on_top_reached = on_top_reached;
tabs[i].body->on_bottom_reached = on_bottom_reached;
@@ -7258,14 +7338,16 @@ namespace QuickMedia {
}
if(event.key.code == mgl::Keyboard::I && !event.key.control) {
- RoomExtraData &room_extra_data = matrix->get_room_extra_data(current_room);
- frame_skip_text_entry = true;
- chat_input.set_editable(true);
- if(get_config().matrix.clear_message_on_escape || !room_extra_data.editing_message_id.empty()) {
- chat_input.set_text("");
- room_extra_data.editing_message_id.clear();
+ start_typing();
+ }
+
+ if(event.key.code == mgl::Keyboard::G && !event.key.control) {
+ start_typing();
+ std::string chat_text = chat_input.get_text();
+ if(!string_starts_with(chat_text, "/encrypt")) {
+ chat_input.set_text("/encrypt " + chat_text);
+ chat_input.set_caret_index(chat_input.get_caret_index() + 9);
}
- chat_state = ChatState::TYPING_MESSAGE;
}
if(event.key.control && event.key.code == mgl::Keyboard::V) {
@@ -7352,21 +7434,27 @@ namespace QuickMedia {
// TODO: Show inline notification
show_notification("QuickMedia", "You can't reply to a message that hasn't been sent yet");
} else {
- RoomExtraData &room_extra_data = matrix->get_room_extra_data(current_room);
- chat_state = ChatState::REPLYING;
- currently_operating_on_item = selected;
- chat_input.set_editable(true);
- if(get_config().matrix.clear_message_on_escape || !room_extra_data.editing_message_id.empty()) {
- chat_input.set_text("");
- room_extra_data.editing_message_id.clear();
- }
- replying_to_text.set_string("Replying to:");
- frame_skip_text_entry = true;
+ start_replying(std::move(selected));
}
}
}
}
+ if(event.key.code == mgl::Keyboard::G && event.key.control) {
+ std::shared_ptr<BodyItem> selected = tabs[selected_tab].body->get_selected_shared();
+ if(static_cast<Message*>(selected->userdata)->event_id.empty()) {
+ // TODO: Show inline notification
+ show_notification("QuickMedia", "You can't reply to a message that hasn't been sent yet");
+ } else {
+ start_replying(std::move(selected));
+ std::string chat_text = chat_input.get_text();
+ if(!string_starts_with(chat_text, "/encrypt")) {
+ chat_input.set_text("/encrypt " + chat_text);
+ chat_input.set_caret_index(chat_input.get_caret_index() + 9);
+ }
+ }
+ }
+
if(event.key.code == mgl::Keyboard::B && event.key.control) {
// Reload room, goes to latest message l0l
move_room = true;
@@ -7391,6 +7479,10 @@ namespace QuickMedia {
} else {
RoomExtraData &room_extra_data = matrix->get_room_extra_data(current_room);
std::string body_text_unformatted = Text::to_printable_string(selected->get_description());
+ MatrixChatBodyItemData *matrix_chat_body_item = static_cast<MatrixChatBodyItemData*>(selected->extra.get());
+ const char *encrypt_prefix = "🔒 ";
+ if(string_starts_with(body_text_unformatted, encrypt_prefix) && matrix_chat_body_item)
+ body_text_unformatted.replace(0, strlen(encrypt_prefix), "/encrypt ");
chat_state = ChatState::EDITING;
currently_operating_on_item = selected;
diff --git a/src/RoundedRectangle.cpp b/src/RoundedRectangle.cpp
index 82ade02..81cf63b 100644
--- a/src/RoundedRectangle.cpp
+++ b/src/RoundedRectangle.cpp
@@ -68,6 +68,6 @@ namespace QuickMedia {
}
}
- target.draw(vertices, 4, mgl::PrimitiveType::Quads, rounded_rectangle_shader);
+ target.draw(vertices, 4, mgl::PrimitiveType::Quads, {0, 0}, rounded_rectangle_shader);
}
} \ No newline at end of file
diff --git a/src/Storage.cpp b/src/Storage.cpp
index dc6b6a2..5c12911 100644
--- a/src/Storage.cpp
+++ b/src/Storage.cpp
@@ -244,12 +244,27 @@ namespace QuickMedia {
return rename_atomic(tmp_path.data.c_str(), path.data.c_str());
}
+ bool file_append(const Path &path, const std::string &data) {
+ FILE *file = fopen_eintr(path.data.c_str(), "ab");
+ if(!file) {
+ perror(path.data.c_str());
+ return false;
+ }
+
+ if(fwrite_eintr(data.data(), data.size(), file) != data.size()) {
+ fclose(file);
+ return false;
+ }
+
+ return fclose(file) == 0;
+ }
+
void for_files_in_dir(const Path &path, FileIteratorCallback callback) {
try {
for(auto &p : std::filesystem::directory_iterator(path.data)) {
std::error_code ec;
const FileType file_type = p.is_directory(ec) ? FileType::DIRECTORY : FileType::REGULAR;
- if(!callback(p.path().string(), file_type))
+ if(!callback(p.path().string(), file_type, 0))
break;
}
} catch(const std::filesystem::filesystem_error &err) {
@@ -258,19 +273,13 @@ namespace QuickMedia {
}
}
- static std::filesystem::file_time_type file_get_filetime_or(const std::filesystem::directory_entry &path, std::filesystem::file_time_type default_value) {
- try {
- return path.last_write_time();
- } catch(const std::filesystem::filesystem_error &err) {
- return default_value;
- }
- }
-
void for_files_in_dir_sort_last_modified(const Path &path, FileIteratorCallback callback, FileSortDirection sort_dir) {
- std::vector<std::filesystem::directory_entry> paths;
+ std::vector<std::pair<std::filesystem::directory_entry, time_t>> paths_with_last_modified;
try {
for(auto &p : std::filesystem::directory_iterator(path.data)) {
- paths.push_back(p);
+ time_t last_modified = 0;
+ file_get_last_modified_time_seconds(p.path().c_str(), &last_modified);
+ paths_with_last_modified.push_back(std::make_pair(p, last_modified));
}
} catch(const std::filesystem::filesystem_error &err) {
fprintf(stderr, "Failed to list files in directory %s, error: %s\n", path.data.c_str(), err.what());
@@ -278,19 +287,19 @@ namespace QuickMedia {
}
if(sort_dir == FileSortDirection::ASC) {
- std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) {
- return file_get_filetime_or(path1, std::filesystem::file_time_type::min()) > file_get_filetime_or(path2, std::filesystem::file_time_type::min());
+ std::sort(paths_with_last_modified.begin(), paths_with_last_modified.end(), [](const auto &path1, const auto &path2) {
+ return path1.second > path2.second;
});
} else {
- std::sort(paths.begin(), paths.end(), [](const std::filesystem::directory_entry &path1, std::filesystem::directory_entry &path2) {
- return file_get_filetime_or(path1, std::filesystem::file_time_type::min()) < file_get_filetime_or(path2, std::filesystem::file_time_type::min());
+ std::sort(paths_with_last_modified.begin(), paths_with_last_modified.end(), [](const auto &path1, const auto &path2) {
+ return path1.second < path2.second;
});
}
- for(auto &p : paths) {
+ for(auto &p : paths_with_last_modified) {
std::error_code ec;
- const FileType file_type = p.is_directory(ec) ? FileType::DIRECTORY : FileType::REGULAR;
- if(!callback(p.path().string(), file_type))
+ const FileType file_type = p.first.is_directory(ec) ? FileType::DIRECTORY : FileType::REGULAR;
+ if(!callback(p.first.path().string(), file_type, p.second))
break;
}
}
@@ -319,7 +328,7 @@ namespace QuickMedia {
for(auto &p : paths) {
std::error_code ec;
const FileType file_type = p.is_directory(ec) ? FileType::DIRECTORY : FileType::REGULAR;
- if(!callback(p.path().string(), file_type))
+ if(!callback(p.path().string(), file_type, 0))
break;
}
}
diff --git a/src/Text.cpp b/src/Text.cpp
index 2b62798..6333e8e 100644
--- a/src/Text.cpp
+++ b/src/Text.cpp
@@ -285,6 +285,10 @@ namespace QuickMedia
int Text::getCaretIndex() const {
return caretIndex;
}
+
+ void Text::setCaretIndex(int index) {
+ caretIndex = index;
+ }
void Text::set_color(mgl::Color color, bool force_color)
{
diff --git a/src/Utils.cpp b/src/Utils.cpp
index ca153ab..e6628d1 100644
--- a/src/Utils.cpp
+++ b/src/Utils.cpp
@@ -2,13 +2,11 @@
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
-#include <locale.h>
+#include <X11/Xlib.h>
namespace QuickMedia {
static bool qm_enable_touch = false;
static bool qm_enable_touch_set = false;
- static bool wayland_display_set = false;
- static const char *wayland_display = nullptr;
void show_virtual_keyboard() {
if(!is_touch_enabled())
@@ -38,14 +36,9 @@ namespace QuickMedia {
return qm_enable_touch;
}
- // TODO: Find a better way to detect this. This will return true on ubuntu when running gnome in x11 mode
- bool is_running_wayland() {
- if(wayland_display_set)
- return wayland_display;
-
- wayland_display = getenv("WAYLAND_DISPLAY");
- wayland_display_set = true;
- return wayland_display;
+ bool is_running_wayland(void *dpy) {
+ int opcode, event, error;
+ return XQueryExtension((Display*)dpy, "XWAYLAND", &opcode, &event, &error);
}
time_t iso_utc_to_unix_time(const char *time_str) {
diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp
index f263fc9..ea18289 100644
--- a/src/VideoPlayer.cpp
+++ b/src/VideoPlayer.cpp
@@ -159,7 +159,7 @@ namespace QuickMedia {
fcntl(ipc_socket, F_SETFL, flags | O_NONBLOCK);
const std::string ipc_fd = std::to_string(fd[1]);
- std::string cache_dir = "--cache-dir=" + std::move(get_cache_dir().join("media").data);
+ std::string cache_dir = "--demuxer-cache-dir=" + std::move(get_cache_dir().join("media").data);
std::string wid_arg = "--wid=";
wid_arg += std::to_string(startup_args.parent_window);
@@ -177,7 +177,6 @@ namespace QuickMedia {
const std::string config_dir = "--config-dir=" + startup_args.resource_root + "mpv";
const std::string input_conf_file = "--input-conf=" + startup_args.resource_root + "mpv/input.conf";
- const std::string ytdl_hook_file = "--scripts=" + startup_args.resource_root + "mpv/scripts/ytdl_hook.lua";
std::vector<const char*> args;
// TODO: Resume playback if the last video played matches the first video played next time QuickMedia is launched
@@ -218,7 +217,7 @@ namespace QuickMedia {
args.push_back("--resume-playback=no");
}
- if(is_running_wayland()) {
+ if(is_running_wayland(display)) {
args.push_back("--gpu-context=x11egl");
fprintf(stderr, "Wayland detected. Launching mpv in x11egl mode\n");
}
@@ -251,27 +250,51 @@ namespace QuickMedia {
if(!startup_args.referer.empty())
args.push_back(referer_arg.c_str());
- std::string mpris_arg;
- Path mpris_path = get_config_dir_xdg().join("mpv").join("scripts").join("mpris.so");
- if(get_file_type(mpris_path) == FileType::REGULAR)
- mpris_arg = "--scripts=" + mpris_path.data;
+ std::string scripts;
+ std::string scripts_arg;
+ const Path mpris_path = get_config_dir_xdg().join("mpv").join("scripts").join("mpris.so");
+ if(get_file_type(mpris_path) == FileType::REGULAR) {
+ if(!scripts.empty())
+ scripts += ":";
+ scripts += mpris_path.data;
+ }
+
+ const Path mpris_system_path = "/etc/mpv/scripts/mpris.so";
+ if(get_file_type(mpris_system_path) == FileType::REGULAR) {
+ if(!scripts.empty())
+ scripts += ":";
+ scripts += mpris_system_path.data;
+ }
+
+ std::string profile_arg;
if(startup_args.use_system_mpv_config) {
+ if(!scripts.empty())
+ scripts += ":";
+ scripts += startup_args.resource_root + "mpv/scripts/ytdl_hook.lua";
+
args.push_back("--config=yes");
args.push_back("--load-scripts=yes");
args.push_back("--osc=yes");
args.push_back(input_conf_file.c_str());
- args.push_back(ytdl_hook_file.c_str());
+
+ if(!startup_args.system_mpv_profile.empty())
+ profile_arg = "--profile=" + startup_args.system_mpv_profile;
} else {
args.insert(args.end(), {
config_dir.c_str(),
"--config=yes"
});
+ }
- if(!mpris_arg.empty())
- args.push_back(mpris_arg.c_str());
+ if(!scripts.empty()) {
+ scripts_arg = "--scripts=" + scripts;
+ args.push_back(scripts_arg.c_str());
}
+ if(!profile_arg.empty())
+ args.push_back(profile_arg.c_str());
+
std::string force_media_title_arg;
if(!startup_args.title.empty()) {
force_media_title_arg = "--force-media-title=" + startup_args.title;
diff --git a/src/main.cpp b/src/main.cpp
index ef0568b..3d675bc 100644
--- a/src/main.cpp
+++ b/src/main.cpp
@@ -1,8 +1,10 @@
#include "../include/QuickMedia.hpp"
#include <locale.h>
+#include <malloc.h>
int main(int argc, char **argv) {
setlocale(LC_ALL, "C"); // Sigh... stupid C
+ mallopt(M_MMAP_THRESHOLD, 65536);
QuickMedia::Program program;
return program.run(argc, argv);
}
diff --git a/src/plugins/DramaCool.cpp b/src/plugins/DramaCool.cpp
index de5357a..f513df3 100644
--- a/src/plugins/DramaCool.cpp
+++ b/src/plugins/DramaCool.cpp
@@ -3,9 +3,25 @@
#include "../../include/StringUtils.hpp"
#include "../../include/M3U8.hpp"
#include "../../plugins/utils/WatchProgress.hpp"
+#include "../../external/cppcodec/base64_rfc4648.hpp"
+
+#define AES256 1
+#define CBC 1
+#define ECB 0
+#define CTR 0
+
+extern "C" {
+#include "utils/aes.h"
+}
+
#include <json/value.h>
+#include <json/reader.h>
#include <quickmedia/HtmlSearch.h>
+// Keys are from: https://github.com/henry-richard7/shows-flix/blob/main/lib/Scraper/vidstream_scraper.dart
+#define ASIANLOAD_KEY "93422192433952489752342908585752"
+#define ASIANLOAD_IV "9262859232435825"
+
// TODO: Add bookmarks page, history, track watch progress, automatically go to next episode, subscribe, etc.
namespace QuickMedia {
@@ -125,6 +141,7 @@ namespace QuickMedia {
struct VideoSources {
//std::string streamsss;
+ std::string asianload;
std::string streamtape;
std::string mixdrop;
std::string mp4upload;
@@ -147,6 +164,7 @@ namespace QuickMedia {
static void dembed_extract_video_sources(const std::string &website_data, VideoSources &video_sources) {
//dembed_extract_video_source(website_data, "streamsss.net", video_sources.streamsss);
+ dembed_extract_video_source(website_data, "asianbxkiun.pro/embedplus?id=", video_sources.asianload);
dembed_extract_video_source(website_data, "streamtape.com", video_sources.streamtape);
dembed_extract_video_source(website_data, "mixdrop.co", video_sources.mixdrop);
dembed_extract_video_source(website_data, "www.mp4upload.com", video_sources.mp4upload);
@@ -201,6 +219,7 @@ namespace QuickMedia {
if(result != DownloadResult::OK)
return false;
+ // TODO: get the resolution that is lower or equal to the height we want
url = M3U8Stream::get_highest_resolution_stream(m3u8_get_streams(website_data)).url;
return true;
}
@@ -400,6 +419,173 @@ namespace QuickMedia {
return true;
}
+ static std::string url_extract_param(const std::string &url, const std::string &param_key) {
+ std::string value;
+ const std::string param = param_key + "=";
+ size_t start_index = url.find(param);
+ if(start_index == std::string::npos)
+ return value;
+
+ start_index += param.size();
+ size_t end_index = url.find('&', start_index);
+ if(end_index == std::string::npos) {
+ end_index = url.find('"', start_index);
+ if(end_index == std::string::npos)
+ return value;
+ }
+
+ value = url.substr(start_index, end_index - start_index);
+ return value;
+ }
+
+ static size_t align_up(size_t value, size_t alignment) {
+ size_t v = value / alignment;
+ if(value % alignment != 0)
+ v++;
+ if(v == 0)
+ v = 1;
+ return v * alignment;
+ }
+
+ // |key| should be a multiple of AES_KEYLEN (32) and |iv| should be a multiple of AES_BLOCKLEN (16)
+ static std::string aes_cbc_encrypt_base64(const std::string &str, const uint8_t *key, const uint8_t *iv) {
+ std::string result;
+
+ const size_t input_size = align_up(str.size(), AES_BLOCKLEN);
+ uint8_t *input = (uint8_t*)malloc(input_size);
+ if(!input)
+ return result;
+
+ memcpy(input, str.data(), str.size());
+
+ // PKCS#7 padding
+ const int num_padded_bytes = input_size - str.size();
+ memset(input + str.size(), num_padded_bytes, num_padded_bytes);
+
+ struct AES_ctx ctx;
+ AES_init_ctx_iv(&ctx, key, iv);
+ AES_CBC_encrypt_buffer(&ctx, input, input_size);
+
+ std::string input_data_str((const char*)input, input_size);
+ result = cppcodec::base64_rfc4648::encode<std::string>(input_data_str);
+ free(input);
+
+ return result;
+ }
+
+ static std::string aes_cbc_decrypt(const std::string &str, const uint8_t *key, const uint8_t *iv) {
+ std::string result;
+
+ const size_t input_size = align_up(str.size(), AES_BLOCKLEN);
+ uint8_t *input = (uint8_t*)malloc(input_size);
+ if(!input)
+ return result;
+
+ memcpy(input, str.data(), str.size());
+
+ // PKCS#7 padding
+ const int num_padded_bytes = input_size - str.size();
+ memset(input + str.size(), num_padded_bytes, num_padded_bytes);
+
+ struct AES_ctx ctx;
+ AES_init_ctx_iv(&ctx, key, iv);
+ AES_CBC_decrypt_buffer(&ctx, input, input_size);
+
+ result.assign((const char*)input, str.size());
+ free(input);
+
+ return result;
+ }
+
+ static std::string asianload_decrypt_response_get_hls_url(const Json::Value &json_result) {
+ std::string url;
+ if(!json_result.isObject())
+ return url;
+
+ const Json::Value &data_json = json_result["data"];
+ if(!data_json.isString())
+ return url;
+
+ std::string data_raw = cppcodec::base64_rfc4648::decode<std::string>(data_json.asString());
+ const std::string input = aes_cbc_decrypt(data_raw, (const uint8_t*)ASIANLOAD_KEY, (const uint8_t*)ASIANLOAD_IV);
+
+ Json::CharReaderBuilder json_builder;
+ std::unique_ptr<Json::CharReader> json_reader(json_builder.newCharReader());
+ std::string json_errors;
+ Json::Value result;
+ if(!json_reader->parse(input.data(), input.data() + data_raw.size(), &result, &json_errors)) {
+ fprintf(stderr, "asianload_decrypt_response_get_hls_url error: %s\n", json_errors.c_str());
+ return url;
+ }
+
+ if(!result.isObject())
+ return url;
+
+ const Json::Value &source_json = result["source"];
+ if(!source_json.isArray())
+ return url;
+
+ // The json data also contains backup (source_bk) and tracks (vtt), but we ignore those for now
+ for(const Json::Value &item_json : source_json) {
+ if(!item_json.isObject())
+ continue;
+
+ const Json::Value &file_json = item_json["file"];
+ const Json::Value &type_json = item_json["type"];
+ if(!file_json.isString() || !type_json.isString())
+ continue;
+
+ if(strcmp(type_json.asCString(), "hls") != 0)
+ continue;
+
+ url = file_json.asString();
+ break;
+ }
+
+ return url;
+ }
+
+ static std::string hls_url_remove_filename(const std::string &hls_url) {
+ std::string result;
+ size_t index = hls_url.rfind('/');
+ if(index == std::string::npos)
+ return result;
+ result = hls_url.substr(0, index);
+ return result;
+ }
+
+ static std::string asianload_get_best_quality_stream(const std::string &hls_url) {
+ std::string url;
+ std::string website_data;
+ DownloadResult result = download_to_string(hls_url, website_data, {}, true);
+ if(result != DownloadResult::OK)
+ return url;
+
+ // TODO: get the resolution that is lower or equal to the height we want
+ url = M3U8Stream::get_highest_resolution_stream(m3u8_get_streams(website_data)).url;
+ return hls_url_remove_filename(hls_url) + "/" + url;
+ }
+
+ static void asianload_get_video_url(Page *page, const std::string &asianload_url, std::string &video_url) {
+ const std::string id = url_extract_param(asianload_url, "id");
+ const std::string token = url_extract_param(asianload_url, "token");
+ const std::string bla = aes_cbc_encrypt_base64(id, (const uint8_t*)ASIANLOAD_KEY, (const uint8_t*)ASIANLOAD_IV);
+
+ const int64_t expires = (int64_t)time(NULL) + (60LL * 60LL); // current time + 1 hour, in seconds
+ const std::string url = "https://asianbxkiun.pro/encrypt-ajax.php?id=" + bla + "&token=" + token + "&expires=" + std::to_string(expires) + "&mip=0.0.0.0&refer=https://asianc.sh/&op=2&alias=" + id;
+
+ Json::Value json_result;
+ DownloadResult result = page->download_json(json_result, url, {{ "-H", "x-requested-with: XMLHttpRequest" }}, true);
+ if(result != DownloadResult::OK)
+ return;
+
+ const std::string hls_url = asianload_decrypt_response_get_hls_url(json_result);
+ if(hls_url.empty())
+ return;
+
+ video_url = asianload_get_best_quality_stream(hls_url);
+ }
+
PluginResult DramaCoolEpisodesPage::submit(const SubmitArgs &args, std::vector<Tab> &result_tabs) {
std::string website_data;
DownloadResult result = download_to_string(args.url, website_data, {}, true);
@@ -453,6 +639,10 @@ namespace QuickMedia {
std::string video_url;
std::string referer;
+ if(!video_sources.asianload.empty() && video_url.empty()) {
+ asianload_get_video_url(this, video_sources.asianload, video_url);
+ }
+
if(!video_sources.streamtape.empty() && video_url.empty()) {
result = download_to_string(video_sources.streamtape, website_data, {}, true);
if(result == DownloadResult::OK) {
diff --git a/src/plugins/LocalAnime.cpp b/src/plugins/LocalAnime.cpp
index c906b9b..0febb23 100644
--- a/src/plugins/LocalAnime.cpp
+++ b/src/plugins/LocalAnime.cpp
@@ -55,7 +55,7 @@ namespace QuickMedia {
return season.name;
} else if(std::holds_alternative<LocalAnimeEpisode>(item)) {
const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item);
- return episode.path.filename();
+ return episode.name;
} else {
return {};
}
@@ -139,13 +139,15 @@ namespace QuickMedia {
std::vector<LocalAnimeItem> grouped_items;
for(LocalAnimeItem &item : items) {
if(std::holds_alternative<LocalAnimeEpisode>(item)) {
- const LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item);
+ LocalAnimeEpisode &episode = std::get<LocalAnimeEpisode>(item);
std::optional<EpisodeNameParts> name_parts = episode_name_extract_parts(episode.path.filename());
if(!name_parts) {
grouped_items.push_back(std::move(item));
continue;
}
+ episode.name = name_parts->episode;
+
GroupedAnime &grouped_anime = anime_by_name[name_parts->anime];
if(grouped_anime.anime.name.empty())
grouped_anime.anime.name = name_parts->anime;
@@ -190,7 +192,7 @@ namespace QuickMedia {
static std::vector<LocalAnimeItem> get_episodes_in_directory(const Path &directory) {
std::vector<LocalAnimeItem> episodes;
- for_files_in_dir_sort_name(directory, [&episodes](const Path &filepath, FileType file_type) -> bool {
+ for_files_in_dir_sort_name(directory, [&episodes](const Path &filepath, FileType file_type, time_t) -> bool {
if(file_type != FileType::REGULAR || !is_video_ext(filepath.ext()))
return true;
@@ -198,7 +200,7 @@ namespace QuickMedia {
if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &modified_time_seconds))
return true;
- episodes.push_back(LocalAnimeEpisode{ filepath, modified_time_seconds });
+ episodes.push_back(LocalAnimeEpisode{ filepath.data, filepath, modified_time_seconds });
return true;
}, FileSortDirection::DESC);
return episodes;
@@ -206,21 +208,28 @@ namespace QuickMedia {
static std::vector<LocalAnimeItem> get_episodes_or_seasons_in_directory(const Path &directory) {
std::vector<LocalAnimeItem> anime_items;
- auto callback = [&](const Path &filepath, FileType file_type) -> bool {
- time_t modified_time_seconds;
- if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &modified_time_seconds))
- return true;
-
+ auto callback = [&](const Path &filepath, FileType file_type, time_t last_modified_seconds) -> bool {
if(file_type == FileType::REGULAR) {
- if(is_video_ext(filepath.ext()))
- anime_items.push_back(LocalAnimeEpisode{ filepath, modified_time_seconds });
+ if(is_video_ext(filepath.ext())) {
+ if(last_modified_seconds == 0) {
+ if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds))
+ return true;
+ }
+
+ anime_items.push_back(LocalAnimeEpisode{ filepath.data, filepath, last_modified_seconds });
+ }
return true;
}
+ if(last_modified_seconds == 0) {
+ if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds))
+ return true;
+ }
+
LocalAnimeSeason season;
season.name = filepath.filename();
season.episodes = get_episodes_in_directory(filepath);
- season.modified_time_seconds = modified_time_seconds;
+ season.modified_time_seconds = last_modified_seconds;
if(season.episodes.empty())
return true;
@@ -236,23 +245,33 @@ namespace QuickMedia {
return anime_items;
}
- std::vector<LocalAnimeItem> get_anime_in_directory(const Path &directory) {
+ std::vector<LocalAnimeItem> get_anime_in_directory(const Path &directory, bool recursive) {
std::vector<LocalAnimeItem> anime_items;
- auto callback = [&anime_items](const Path &filepath, FileType file_type) -> bool {
- time_t modified_time_seconds;
- if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &modified_time_seconds))
- return true;
-
+ auto callback = [&anime_items, recursive](const Path &filepath, FileType file_type, time_t last_modified_seconds) -> bool {
if(file_type == FileType::REGULAR) {
- if(is_video_ext(filepath.ext()))
- anime_items.push_back(LocalAnimeEpisode{ filepath, modified_time_seconds });
+ if(is_video_ext(filepath.ext())) {
+ if(last_modified_seconds == 0) {
+ if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds))
+ return true;
+ }
+
+ anime_items.push_back(LocalAnimeEpisode{ filepath.data, filepath, last_modified_seconds });
+ }
return true;
}
+
+ if(!recursive)
+ return true;
+ if(last_modified_seconds == 0) {
+ if(!file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds))
+ return true;
+ }
+
LocalAnime anime;
anime.name = filepath.filename();
anime.items = get_episodes_or_seasons_in_directory(filepath);
- anime.modified_time_seconds = modified_time_seconds;
+ anime.modified_time_seconds = last_modified_seconds;
if(anime.items.empty())
return true;
@@ -352,7 +371,7 @@ namespace QuickMedia {
PluginResult LocalAnimeSearchPage::lazy_fetch(BodyItems &result_items) {
if(fetch_home_page) {
fetch_home_page = false;
- anime_items = get_anime_in_directory(get_config().local_anime.directory);
+ anime_items = get_anime_in_directory(get_config().local_anime.directory, get_config().local_anime.recursive);
}
std::unordered_map<std::string, WatchProgress> watch_progress = get_watch_progress_for_plugin("local-anime");
diff --git a/src/plugins/LocalManga.cpp b/src/plugins/LocalManga.cpp
index fa0df27..21e8780 100644
--- a/src/plugins/LocalManga.cpp
+++ b/src/plugins/LocalManga.cpp
@@ -83,7 +83,7 @@ namespace QuickMedia {
// Pages are sorted from 1.png to n.png
static std::vector<LocalMangaPage> get_images_in_manga(const Path &directory) {
std::vector<LocalMangaPage> page_list;
- for_files_in_dir(directory, [&page_list](const Path &filepath, FileType file_type) -> bool {
+ for_files_in_dir(directory, [&page_list](const Path &filepath, FileType file_type, time_t) -> bool {
if(file_type != FileType::REGULAR)
return true;
@@ -107,7 +107,7 @@ namespace QuickMedia {
static std::vector<LocalMangaChapter> get_chapters_in_manga(std::string manga_name, const Path &directory, bool only_include_latest, bool include_pages, bool only_get_coverpage = false) {
std::vector<LocalMangaChapter> chapter_list;
- auto callback = [&chapter_list, &manga_name, only_include_latest, include_pages, only_get_coverpage](const Path &filepath, FileType file_type) -> bool {
+ auto callback = [&chapter_list, &manga_name, only_include_latest, include_pages, only_get_coverpage](const Path &filepath, FileType file_type, time_t last_modified_seconds) -> bool {
if(file_type != FileType::DIRECTORY)
return true;
@@ -130,8 +130,11 @@ namespace QuickMedia {
}
}
- if(!only_get_coverpage)
- file_get_last_modified_time_seconds(filepath.data.c_str(), &local_manga_chapter.modified_time_seconds);
+ if(!only_get_coverpage) {
+ if(last_modified_seconds == 0)
+ file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds);
+ local_manga_chapter.modified_time_seconds = last_modified_seconds;
+ }
chapter_list.push_back(std::move(local_manga_chapter));
return only_include_latest ? false : true;
@@ -147,16 +150,20 @@ namespace QuickMedia {
static std::vector<LocalManga> get_manga_in_directory(const Path &directory, bool only_get_coverpage) {
std::vector<LocalManga> manga_list;
- auto callback = [&manga_list, only_get_coverpage](const Path &filepath, FileType file_type) -> bool {
+ auto callback = [&manga_list, only_get_coverpage](const Path &filepath, FileType file_type, time_t last_modified_seconds) -> bool {
if(file_type != FileType::DIRECTORY)
return true;
LocalManga local_manga;
local_manga.name = filepath.filename();
local_manga.chapters = get_chapters_in_manga(local_manga.name, filepath, true, false, only_get_coverpage);
- if(local_manga.chapters.empty() || !file_get_last_modified_time_seconds(filepath.data.c_str(), &local_manga.modified_time_seconds))
+ if(local_manga.chapters.empty())
return true;
+ if(last_modified_seconds == 0)
+ file_get_last_modified_time_seconds(filepath.data.c_str(), &last_modified_seconds);
+ local_manga.modified_time_seconds = last_modified_seconds;
+
manga_list.push_back(std::move(local_manga));
return true;
};
diff --git a/src/plugins/Matrix.cpp b/src/plugins/Matrix.cpp
index c4aa076..c810d88 100644
--- a/src/plugins/Matrix.cpp
+++ b/src/plugins/Matrix.cpp
@@ -629,12 +629,15 @@ namespace QuickMedia {
rooms_page->add_body_item(body_item);
}
- void MatrixQuickMedia::leave_room(RoomData *room, LeaveType leave_type, const std::string &reason) {
+ void MatrixQuickMedia::leave_room(RoomData *room, const std::string &event_id, LeaveType leave_type, const std::string &reason) {
room_body_item_by_room.erase(room);
rooms_page->remove_body_item_by_room_id(room->id);
room_tags_page->remove_body_item_by_room_id(room->id);
- if(leave_type != LeaveType::LEAVE)
+ if(leave_type != LeaveType::LEAVE && (event_id.empty() || !matrix->is_other_notification_read(event_id))) {
show_notification("QuickMedia", reason);
+ if(!event_id.empty())
+ matrix->mark_other_notification_as_read(event_id);
+ }
}
void MatrixQuickMedia::room_add_tag(RoomData *room, const std::string &tag) {
@@ -1367,6 +1370,35 @@ namespace QuickMedia {
}
}
+ const char* MatrixVideoPage::get_title() const {
+ return "";
+ }
+
+ std::string MatrixVideoPage::get_filename() {
+ return filename;
+ }
+
+ std::string MatrixVideoPage::get_download_url_for_clipboard(int max_height) {
+ std::string download_url = get_download_url(max_height);
+ size_t index = download_url.find("?access_token=");
+ if(index == std::string::npos) {
+ index = download_url.find("&access_token=");
+ if(index == std::string::npos)
+ return download_url;
+ }
+
+ size_t end = download_url.find('&', index + 14);
+ if(end == std::string::npos) {
+ end = download_url.size();
+ } else {
+ ++index;
+ ++end;
+ }
+
+ download_url.erase(index, end - index);
+ return download_url;
+ }
+
MatrixChatPage::MatrixChatPage(Program *program, std::string room_id, MatrixRoomsPage *rooms_page, std::string jump_to_event_id) :
Page(program), room_id(std::move(room_id)), rooms_page(rooms_page), jump_to_event_id(std::move(jump_to_event_id))
{
@@ -1674,6 +1706,7 @@ namespace QuickMedia {
load_silenced_invites();
load_custom_emoji_from_cache();
+ load_other_notifications();
sync_thread = std::thread([this, matrix_cache_dir]() {
FILE *sync_cache_file;
@@ -1858,8 +1891,8 @@ namespace QuickMedia {
fwrite(json_data.data(), 1, json_data.size(), sync_cache_file);
file_overwrite(get_cache_dir().join("matrix").join(update_cache_file_name), "1"); // To make sure the cache format is up to date
- malloc_trim(0);
}
+ malloc_trim(0);
fclose(sync_cache_file);
}
}
@@ -2208,6 +2241,18 @@ namespace QuickMedia {
parse_custom_emoji(json_root);
}
+ void Matrix::load_other_notifications() {
+ std::string result;
+ if(file_get_content(get_storage_dir().join("matrix").join("other_notifications_read"), result) != 0)
+ return;
+
+ other_notifications_read.clear();
+ string_split_view(result, '\n', [this](const char *str, size_t line) {
+ other_notifications_read.insert(std::string(str, line));
+ return true;
+ });
+ }
+
PluginResult Matrix::parse_sync_account_data(const rapidjson::Value &account_data_json) {
if(!account_data_json.IsObject())
return PluginResult::OK;
@@ -2458,16 +2503,16 @@ namespace QuickMedia {
return media_url.substr(start, end - start);
}
- static std::string get_avatar_thumbnail_url(const std::string &homeserver, const std::string &mxc_id) {
+ static std::string get_avatar_thumbnail_url(const std::string &homeserver, const std::string &access_token, const std::string &mxc_id) {
if(mxc_id.empty())
return "";
std::string size = std::to_string(int(32 * get_config().scale));
- return homeserver + "/_matrix/media/r0/thumbnail/" + mxc_id + "?width=" + size + "&height=" + size + "&method=crop";
+ return homeserver + "/_matrix/client/v1/media/thumbnail/" + mxc_id + "?width=" + size + "&height=" + size + "&method=crop&access_token=" + access_token;
}
std::string Matrix::get_media_url(const std::string &mxc_id) {
- return homeserver + "/_matrix/media/r0/download/" + thumbnail_url_extract_media_id(mxc_id);
+ return homeserver + "/_matrix/client/v1/media/download/" + thumbnail_url_extract_media_id(mxc_id) + "?access_token=" + access_token;
}
RoomExtraData& Matrix::get_room_extra_data(RoomData *room) {
@@ -2486,7 +2531,7 @@ namespace QuickMedia {
std::string display_name = display_name_json.IsString() ? display_name_json.GetString() : user_id;
std::string avatar_url = thumbnail_url_extract_media_id(avatar_url_str);
if(!avatar_url.empty())
- avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
+ avatar_url = get_avatar_thumbnail_url(homeserver, access_token, avatar_url); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
//auto user_info = std::make_shared<UserInfo>(room_data, user_id, std::move(display_name), std::move(avatar_url));
// Overwrites user data
//room_data->add_user(user_info);
@@ -2583,7 +2628,7 @@ namespace QuickMedia {
return found_resolution;
}
- static std::string message_content_extract_thumbnail_url(const rapidjson::Value &content_json, const std::string &homeserver) {
+ static std::string message_content_extract_thumbnail_url(const rapidjson::Value &content_json, const std::string &homeserver, const std::string &access_token) {
const rapidjson::Value &info_json = GetMember(content_json, "info");
if(info_json.IsObject()) {
const rapidjson::Value &thumbnail_url_json = GetMember(info_json, "thumbnail_url");
@@ -2593,7 +2638,7 @@ namespace QuickMedia {
return "";
thumbnail_str.erase(thumbnail_str.begin(), thumbnail_str.begin() + 6);
- return homeserver + "/_matrix/media/r0/download/" + std::move(thumbnail_str);
+ return homeserver + "/_matrix/client/v1/media/download/" + std::move(thumbnail_str) + "?access_token=" + access_token;
}
}
@@ -3180,7 +3225,7 @@ namespace QuickMedia {
body = user_display_name + " changed his profile picture";
std::string new_avatar_url_str = thumbnail_url_extract_media_id(new_avatar_url_json.GetString());
if(!new_avatar_url_str.empty())
- new_avatar_url_str = get_avatar_thumbnail_url(homeserver, new_avatar_url_str); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
+ new_avatar_url_str = get_avatar_thumbnail_url(homeserver, access_token, new_avatar_url_str); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
new_avatar_url = new_avatar_url_str;
update_user_display_info = room_data->set_user_avatar_url(user, std::move(new_avatar_url_str), timestamp);
} else if((!new_avatar_url_json.IsString() || new_avatar_url_json.GetStringLength() == 0) && prev_avatar_url_json.IsString()) {
@@ -3406,8 +3451,8 @@ namespace QuickMedia {
if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0)
return nullptr;
- message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6);
- message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver);
+ message->url = homeserver + "/_matrix/client/v1/media/download/" + (url_json.GetString() + 6) + "?access_token=" + access_token;
+ message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver, access_token);
message_content_extract_thumbnail_size(*content_json, message->thumbnail_size);
message->type = MessageType::IMAGE;
} else if(!content_type.IsString() || strcmp(content_type.GetString(), "m.text") == 0) {
@@ -3417,8 +3462,8 @@ namespace QuickMedia {
if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0)
return nullptr;
- message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6);
- message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver);
+ message->url = homeserver + "/_matrix/client/v1/media/download/" + (url_json.GetString() + 6) + "?access_token=" + access_token;
+ message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver, access_token);
message_content_extract_thumbnail_size(*content_json, message->thumbnail_size);
message->type = MessageType::VIDEO;
if(message->thumbnail_url.empty())
@@ -3428,7 +3473,7 @@ namespace QuickMedia {
if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0)
return nullptr;
- message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6);
+ message->url = homeserver + "/_matrix/client/v1/media/download/" + (url_json.GetString() + 6) + "?access_token=" + access_token;
message->type = MessageType::AUDIO;
prefix = "🎵 Play ";
} else if(strcmp(content_type.GetString(), "m.file") == 0) {
@@ -3436,7 +3481,7 @@ namespace QuickMedia {
if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0)
return nullptr;
- message->url = homeserver + "/_matrix/media/r0/download/" + (url_json.GetString() + 6);
+ message->url = homeserver + "/_matrix/client/v1/media/download/" + (url_json.GetString() + 6) + "?access_token=" + access_token;
message->type = MessageType::FILE;
prefix = "💾 Download ";
} else if(strcmp(content_type.GetString(), "m.emote") == 0) { // this is a /me message, TODO: show /me messages differently
@@ -3451,7 +3496,7 @@ namespace QuickMedia {
prefix = geo_uri_json.GetString() + std::string(" | ");
message->type = MessageType::TEXT;
- message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver);
+ message->thumbnail_url = message_content_extract_thumbnail_url(*content_json, homeserver, access_token);
message_content_extract_thumbnail_size(*content_json, message->thumbnail_size);
} else if(strcmp(content_type.GetString(), "m.server_notice") == 0) { // TODO: show server notices differently
message->type = MessageType::TEXT;
@@ -3611,7 +3656,7 @@ namespace QuickMedia {
if(!url_json.IsString() || strncmp(url_json.GetString(), "mxc://", 6) != 0)
continue;
- update_room_avatar_url |= room_data->set_avatar_url(get_avatar_thumbnail_url(homeserver, thumbnail_url_extract_media_id(url_json.GetString())), item_timestamp);
+ update_room_avatar_url |= room_data->set_avatar_url(get_avatar_thumbnail_url(homeserver, access_token, thumbnail_url_extract_media_id(url_json.GetString())), item_timestamp);
room_data->avatar_is_fallback = false;
} else if(strcmp(type_json.GetString(), "m.room.topic") == 0) {
const rapidjson::Value &content_json = GetMember(event_item_json, "content");
@@ -3884,6 +3929,10 @@ namespace QuickMedia {
if(!type_json.IsString() || strcmp(type_json.GetString(), "m.room.member") != 0)
continue;
+ const rapidjson::Value &event_id_json = GetMember(event_json, "event_id");
+ if(!event_id_json.IsString())
+ continue;
+
const rapidjson::Value &sender_json = GetMember(event_json, "sender");
if(!sender_json.IsString())
continue;
@@ -3924,7 +3973,10 @@ namespace QuickMedia {
if(!reason_str.empty())
desc += ", reason: " + reason_str;
- ui_thread_tasks.push([this, room, leave_type, desc{std::move(desc)}]{ delegate->leave_room(room, leave_type, desc); });
+ std::string event_id_str = event_id_json.GetString();
+ ui_thread_tasks.push([this, room, event_id_str{std::move(event_id_str)}, leave_type, desc{std::move(desc)}]{
+ delegate->leave_room(room, event_id_str, leave_type, desc);
+ });
remove_room(room_id_str);
break;
}
@@ -4213,6 +4265,15 @@ namespace QuickMedia {
return delegate;
}
+ void Matrix::mark_other_notification_as_read(const std::string &event_id) {
+ other_notifications_read.insert(event_id);
+ file_append(get_storage_dir().join("matrix").join("other_notifications_read"), event_id + "\n");
+ }
+
+ bool Matrix::is_other_notification_read(const std::string &event_id) const {
+ return other_notifications_read.find(event_id) != other_notifications_read.end();
+ }
+
PluginResult Matrix::post_message(RoomData *room, const std::string &body, std::string &event_id_response, const std::optional<UploadInfo> &file_info, const std::optional<UploadInfo> &thumbnail_info, const std::string &msgtype, const std::string &custom_transaction_id) {
std::string transaction_id = custom_transaction_id;
if(transaction_id.empty())
@@ -5040,7 +5101,10 @@ namespace QuickMedia {
remote_homeserver_url = get_remote_homeserver_url();
char url[512];
- snprintf(url, sizeof(url), "%s/_matrix/media/r0/upload?filename=%s", remote_homeserver_url.c_str(), filename_escaped.c_str());
+ snprintf(url, sizeof(url), "%s%s_matrix/media/r0/upload?filename=%s",
+ remote_homeserver_url.c_str(),
+ !remote_homeserver_url.empty() && remote_homeserver_url.back() == '/' ? "" : "/",
+ filename_escaped.c_str());
rapidjson::Document json_root;
DownloadResult download_result = download_json(json_root, url, std::move(additional_args), true, &err_msg);
@@ -5102,7 +5166,7 @@ namespace QuickMedia {
//Path filter_cache_path = get_storage_dir().join("matrix").join("filter");
//remove(filter_cache_path.data.c_str());
- for_files_in_dir(get_cache_dir().join("matrix").join("events"), [](const Path &filepath, FileType) {
+ for_files_in_dir(get_cache_dir().join("matrix").join("events"), [](const Path &filepath, FileType, time_t) {
remove(filepath.data.c_str());
return true;
});
@@ -5629,7 +5693,7 @@ namespace QuickMedia {
if(download_result == DownloadResult::OK) {
RoomData *room = get_room_by_id(room_id);
if(room) {
- ui_thread_tasks.push([this, room]{ delegate->leave_room(room, LeaveType::LEAVE, ""); });
+ ui_thread_tasks.push([this, room]{ delegate->leave_room(room, "", LeaveType::LEAVE, ""); });
remove_room(room_id);
}
}
@@ -5746,7 +5810,7 @@ namespace QuickMedia {
if(avatar_url_json.IsString()) {
std::string avatar_url = thumbnail_url_extract_media_id(avatar_url_json.GetString());
if(!avatar_url.empty())
- avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url);
+ avatar_url = get_avatar_thumbnail_url(homeserver, access_token, avatar_url);
if(!avatar_url.empty())
room_body_item->thumbnail_url = std::move(avatar_url);
@@ -5818,7 +5882,7 @@ namespace QuickMedia {
if(avatar_url_json.IsString()) {
std::string avatar_url = thumbnail_url_extract_media_id(std::string(avatar_url_json.GetString(), avatar_url_json.GetStringLength()));
if(!avatar_url.empty())
- avatar_url = get_avatar_thumbnail_url(homeserver, avatar_url);
+ avatar_url = get_avatar_thumbnail_url(homeserver, access_token, avatar_url);
body_item->thumbnail_url = std::move(avatar_url);
}
body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE;
@@ -6057,7 +6121,7 @@ namespace QuickMedia {
if(avatar_url_json.IsString())
avatar_url = std::string(avatar_url_json.GetString(), avatar_url_json.GetStringLength());
if(!avatar_url.empty())
- avatar_url = get_avatar_thumbnail_url(homeserver, thumbnail_url_extract_media_id(avatar_url)); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
+ avatar_url = get_avatar_thumbnail_url(homeserver, access_token, thumbnail_url_extract_media_id(avatar_url)); // TODO: Remove the constant strings around to reduce memory usage (6.3mb)
room->set_user_avatar_url(user, avatar_url, 0);
room->set_user_display_name(user, display_name, 0);
diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp
index 2c20f3f..7095e6b 100644
--- a/src/plugins/Youtube.cpp
+++ b/src/plugins/Youtube.cpp
@@ -21,7 +21,7 @@ extern "C" {
#include <unistd.h>
namespace QuickMedia {
- static const char *youtube_client_version = "x-youtube-client-version: 2.20210622.10.00";
+ static const char *youtube_client_version = "x-youtube-client-version: 2.20250327.01.00";
static const std::array<std::string, 11> invidious_urls = {
"yewtu.be",
"invidious.snopyta.org",
@@ -3189,26 +3189,33 @@ namespace QuickMedia {
}
}
- const Json::Value &playability_status_json = json_root["playabilityStatus"];
- if(playability_status_json.isObject()) {
- const Json::Value &status_json = playability_status_json["status"];
- if(status_json.isString() && (strcmp(status_json.asCString(), "UNPLAYABLE") == 0 || strcmp(status_json.asCString(), "LOGIN_REQUIRED") == 0)) {
- fprintf(stderr, "Failed to load youtube video, trying with yt-dlp instead\n");
- if(program->youtube_dl_extract_url(url, youtube_dl_video_fallback_url, youtube_dl_audio_fallback_url)) {
- if(get_config().youtube.sponsorblock.enable)
- sponsorblock_add_chapters(this, url, get_config().youtube.sponsorblock.min_votes, sponsor_segments);
- use_youtube_dl_fallback = true;
- return PluginResult::OK;
- } else {
- const Json::Value &reason_json = playability_status_json["reason"];
- if(reason_json.isString())
- err_str = reason_json.asString();
- fprintf(stderr, "Unable to play video, status: %s, reason: %s\n", status_json.asCString(), reason_json.isString() ? reason_json.asCString() : "Unknown");
- return PluginResult::ERR;
- }
- }
+ if(program->youtube_dl_extract_url(url, youtube_dl_video_fallback_url, youtube_dl_audio_fallback_url)) {
+ if(get_config().youtube.sponsorblock.enable)
+ sponsorblock_add_chapters(this, url, get_config().youtube.sponsorblock.min_votes, sponsor_segments);
+ use_youtube_dl_fallback = true;
+ return PluginResult::OK;
}
+ // const Json::Value &playability_status_json = json_root["playabilityStatus"];
+ // if(playability_status_json.isObject()) {
+ // const Json::Value &status_json = playability_status_json["status"];
+ // if(status_json.isString() && (strcmp(status_json.asCString(), "UNPLAYABLE") == 0 || strcmp(status_json.asCString(), "LOGIN_REQUIRED") == 0)) {
+ // fprintf(stderr, "Failed to load youtube video, trying with yt-dlp instead\n");
+ // if(program->youtube_dl_extract_url(url, youtube_dl_video_fallback_url, youtube_dl_audio_fallback_url)) {
+ // if(get_config().youtube.sponsorblock.enable)
+ // sponsorblock_add_chapters(this, url, get_config().youtube.sponsorblock.min_votes, sponsor_segments);
+ // use_youtube_dl_fallback = true;
+ // return PluginResult::OK;
+ // } else {
+ // const Json::Value &reason_json = playability_status_json["reason"];
+ // if(reason_json.isString())
+ // err_str = reason_json.asString();
+ // fprintf(stderr, "Unable to play video, status: %s, reason: %s\n", status_json.asCString(), reason_json.isString() ? reason_json.asCString() : "Unknown");
+ // return PluginResult::ERR;
+ // }
+ // }
+ // }
+
const Json::Value *streaming_data_json = &json_root["streamingData"];
if(!streaming_data_json->isObject())
return PluginResult::ERR;
@@ -3254,7 +3261,51 @@ namespace QuickMedia {
const int num_request_types = 1;
std::string request_data[num_request_types] = {
R"END(
- {"context":{"client":{"hl":"en","gl":"US","clientName":"IOS","clientVersion":"17.33.2","deviceModel":"iPhone14,3"}},"videoId":"%VIDEO_ID%"}
+{
+ "context": {
+ "client": {
+ "hl": "en",
+ "gl": "SE",
+ "deviceMake": "",
+ "deviceModel": "",
+ "userAgent": "Mozilla/5.0 (X11; Linux x86_64; rv:136.0) Gecko/20100101 Firefox/136.0,gzip(gfe)",
+ "clientName": "WEB",
+ "clientVersion": "2.20250327.01.00",
+ "osName": "X11",
+ "osVersion": "",
+ "originalUrl": "https://www.youtube.com/watch?v=%VIDEO_ID%",
+ "platform": "DESKTOP",
+ "clientFormFactor": "UNKNOWN_FORM_FACTOR",
+ "userInterfaceTheme": "USER_INTERFACE_THEME_DARK",
+ "timeZone": "Europe/Paris",
+ "browserName": "Firefox",
+ "browserVersion": "136.0",
+ "acceptHeader": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "clientScreen": "WATCH",
+ "mainAppWebInfo": {
+ "graftUrl": "/watch?v=%VIDEO_ID%",
+ "pwaInstallabilityStatus": "PWA_INSTALLABILITY_STATUS_UNKNOWN",
+ "webDisplayMode": "WEB_DISPLAY_MODE_BROWSER",
+ "isWebNativeShareAvailable": false
+ }
+ }
+ },
+ "videoId": "%VIDEO_ID%",
+ "playbackContext": {
+ "contentPlaybackContext": {
+ "currentUrl": "/watch?v=%VIDEO_ID%",
+ "vis": 0,
+ "splay": false,
+ "autoCaptionsDefaultOn": false,
+ "autonavState": "STATE_OFF",
+ "html5Preference": "HTML5_PREF_WANTS",
+ "referer": "https://www.youtube.com/",
+ "lactMilliseconds": "-1"
+ }
+ },
+ "racyCheckOk": false,
+ "contentCheckOk": false
+}
)END",
};
@@ -3263,7 +3314,7 @@ R"END(
};
std::string client_versions[num_request_types] = {
- "2.20210622.10.00"
+ "2.20250327.01.00"
};
for(int i = 0; i < num_request_types; ++i) {
@@ -3280,7 +3331,7 @@ R"END(
additional_args.insert(additional_args.end(), cookies.begin(), cookies.end());
Json::Value json_root;
- DownloadResult download_result = download_json(json_root, "https://www.youtube.com/youtubei/v1/player?key=" + api_key + "&gl=US&hl=en&prettyPrint=false", additional_args, true);
+ DownloadResult download_result = download_json(json_root, "https://www.youtube.com/youtubei/v1/player?prettyPrint=false", additional_args, true);
if(download_result != DownloadResult::OK)
continue;
@@ -3423,6 +3474,7 @@ R"END(
continue;
}
+ //video_format.base.url += "&alr=yes";
video_formats.push_back(std::move(video_format));
} else if(strncmp(youtube_format_base.mime_type.c_str(), "audio/", 6) == 0) {
// Some youtube videos have multiple audio tracks and sometimes the audio tracks are in the same language
@@ -3445,6 +3497,7 @@ R"END(
continue;
}
+ //audio_format.base.url += "&alr=yes";
audio_formats.push_back(std::move(audio_format));
}
}
diff --git a/src/plugins/utils/EpisodeNameParser.cpp b/src/plugins/utils/EpisodeNameParser.cpp
index 8610ab0..8141421 100644
--- a/src/plugins/utils/EpisodeNameParser.cpp
+++ b/src/plugins/utils/EpisodeNameParser.cpp
@@ -108,6 +108,12 @@ namespace QuickMedia {
return (c >= '0' && c <= '9') || c == '.';
}
+ static void remove_trailing_characters(std::string_view &str, char c) {
+ while(!str.empty() && str.back() == c) {
+ str.remove_suffix(1);
+ }
+ }
+
static std::string_view episode_name_extract_episode(std::string_view &episode_name) {
episode_name = strip_left(episode_name);
size_t i = 0;
@@ -120,6 +126,7 @@ namespace QuickMedia {
return {};
std::string_view episode = episode_name.substr(0, i);
+ remove_trailing_characters(episode, '.');
episode_name.remove_prefix(i + 1);
return episode;
}
diff --git a/src/plugins/utils/aes.c b/src/plugins/utils/aes.c
new file mode 100644
index 0000000..2bd115e
--- /dev/null
+++ b/src/plugins/utils/aes.c
@@ -0,0 +1,571 @@
+/*
+
+This is an implementation of the AES algorithm, specifically ECB, CTR and CBC mode.
+Block size can be chosen in aes.h - available choices are AES128, AES192, AES256.
+
+The implementation is verified against the test vectors in:
+ National Institute of Standards and Technology Special Publication 800-38A 2001 ED
+
+ECB-AES128
+----------
+
+ plain-text:
+ 6bc1bee22e409f96e93d7e117393172a
+ ae2d8a571e03ac9c9eb76fac45af8e51
+ 30c81c46a35ce411e5fbc1191a0a52ef
+ f69f2445df4f9b17ad2b417be66c3710
+
+ key:
+ 2b7e151628aed2a6abf7158809cf4f3c
+
+ resulting cipher
+ 3ad77bb40d7a3660a89ecaf32466ef97
+ f5d3d58503b9699de785895a96fdbaaf
+ 43b1cd7f598ece23881b00e3ed030688
+ 7b0c785e27e8ad3f8223207104725dd4
+
+
+NOTE: String length must be evenly divisible by 16byte (str_len % 16 == 0)
+ You should pad the end of the string with zeros if this is not the case.
+ For AES192/256 the key size is proportionally larger.
+
+*/
+
+
+/*****************************************************************************/
+/* Includes: */
+/*****************************************************************************/
+#include <string.h> // CBC mode, for memset
+#include "aes.h"
+
+/*****************************************************************************/
+/* Defines: */
+/*****************************************************************************/
+// The number of columns comprising a state in AES. This is a constant in AES. Value=4
+#define Nb 4
+
+#if defined(AES256) && (AES256 == 1)
+ #define Nk 8
+ #define Nr 14
+#elif defined(AES192) && (AES192 == 1)
+ #define Nk 6
+ #define Nr 12
+#else
+ #define Nk 4 // The number of 32 bit words in a key.
+ #define Nr 10 // The number of rounds in AES Cipher.
+#endif
+
+// jcallan@github points out that declaring Multiply as a function
+// reduces code size considerably with the Keil ARM compiler.
+// See this link for more information: https://github.com/kokke/tiny-AES-C/pull/3
+#ifndef MULTIPLY_AS_A_FUNCTION
+ #define MULTIPLY_AS_A_FUNCTION 0
+#endif
+
+
+
+
+/*****************************************************************************/
+/* Private variables: */
+/*****************************************************************************/
+// state - array holding the intermediate results during decryption.
+typedef uint8_t state_t[4][4];
+
+
+
+// The lookup-tables are marked const so they can be placed in read-only storage instead of RAM
+// The numbers below can be computed dynamically trading ROM for RAM -
+// This can be useful in (embedded) bootloader applications, where ROM is often limited.
+static const uint8_t sbox[256] = {
+ //0 1 2 3 4 5 6 7 8 9 A B C D E F
+ 0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76,
+ 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0,
+ 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15,
+ 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75,
+ 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84,
+ 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf,
+ 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8,
+ 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2,
+ 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73,
+ 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb,
+ 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79,
+ 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08,
+ 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a,
+ 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e,
+ 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf,
+ 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16 };
+
+#if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1)
+static const uint8_t rsbox[256] = {
+ 0x52, 0x09, 0x6a, 0xd5, 0x30, 0x36, 0xa5, 0x38, 0xbf, 0x40, 0xa3, 0x9e, 0x81, 0xf3, 0xd7, 0xfb,
+ 0x7c, 0xe3, 0x39, 0x82, 0x9b, 0x2f, 0xff, 0x87, 0x34, 0x8e, 0x43, 0x44, 0xc4, 0xde, 0xe9, 0xcb,
+ 0x54, 0x7b, 0x94, 0x32, 0xa6, 0xc2, 0x23, 0x3d, 0xee, 0x4c, 0x95, 0x0b, 0x42, 0xfa, 0xc3, 0x4e,
+ 0x08, 0x2e, 0xa1, 0x66, 0x28, 0xd9, 0x24, 0xb2, 0x76, 0x5b, 0xa2, 0x49, 0x6d, 0x8b, 0xd1, 0x25,
+ 0x72, 0xf8, 0xf6, 0x64, 0x86, 0x68, 0x98, 0x16, 0xd4, 0xa4, 0x5c, 0xcc, 0x5d, 0x65, 0xb6, 0x92,
+ 0x6c, 0x70, 0x48, 0x50, 0xfd, 0xed, 0xb9, 0xda, 0x5e, 0x15, 0x46, 0x57, 0xa7, 0x8d, 0x9d, 0x84,
+ 0x90, 0xd8, 0xab, 0x00, 0x8c, 0xbc, 0xd3, 0x0a, 0xf7, 0xe4, 0x58, 0x05, 0xb8, 0xb3, 0x45, 0x06,
+ 0xd0, 0x2c, 0x1e, 0x8f, 0xca, 0x3f, 0x0f, 0x02, 0xc1, 0xaf, 0xbd, 0x03, 0x01, 0x13, 0x8a, 0x6b,
+ 0x3a, 0x91, 0x11, 0x41, 0x4f, 0x67, 0xdc, 0xea, 0x97, 0xf2, 0xcf, 0xce, 0xf0, 0xb4, 0xe6, 0x73,
+ 0x96, 0xac, 0x74, 0x22, 0xe7, 0xad, 0x35, 0x85, 0xe2, 0xf9, 0x37, 0xe8, 0x1c, 0x75, 0xdf, 0x6e,
+ 0x47, 0xf1, 0x1a, 0x71, 0x1d, 0x29, 0xc5, 0x89, 0x6f, 0xb7, 0x62, 0x0e, 0xaa, 0x18, 0xbe, 0x1b,
+ 0xfc, 0x56, 0x3e, 0x4b, 0xc6, 0xd2, 0x79, 0x20, 0x9a, 0xdb, 0xc0, 0xfe, 0x78, 0xcd, 0x5a, 0xf4,
+ 0x1f, 0xdd, 0xa8, 0x33, 0x88, 0x07, 0xc7, 0x31, 0xb1, 0x12, 0x10, 0x59, 0x27, 0x80, 0xec, 0x5f,
+ 0x60, 0x51, 0x7f, 0xa9, 0x19, 0xb5, 0x4a, 0x0d, 0x2d, 0xe5, 0x7a, 0x9f, 0x93, 0xc9, 0x9c, 0xef,
+ 0xa0, 0xe0, 0x3b, 0x4d, 0xae, 0x2a, 0xf5, 0xb0, 0xc8, 0xeb, 0xbb, 0x3c, 0x83, 0x53, 0x99, 0x61,
+ 0x17, 0x2b, 0x04, 0x7e, 0xba, 0x77, 0xd6, 0x26, 0xe1, 0x69, 0x14, 0x63, 0x55, 0x21, 0x0c, 0x7d };
+#endif
+
+// The round constant word array, Rcon[i], contains the values given by
+// x to the power (i-1) being powers of x (x is denoted as {02}) in the field GF(2^8)
+static const uint8_t Rcon[11] = {
+ 0x8d, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36 };
+
+/*
+ * Jordan Goulder points out in PR #12 (https://github.com/kokke/tiny-AES-C/pull/12),
+ * that you can remove most of the elements in the Rcon array, because they are unused.
+ *
+ * From Wikipedia's article on the Rijndael key schedule @ https://en.wikipedia.org/wiki/Rijndael_key_schedule#Rcon
+ *
+ * "Only the first some of these constants are actually used – up to rcon[10] for AES-128 (as 11 round keys are needed),
+ * up to rcon[8] for AES-192, up to rcon[7] for AES-256. rcon[0] is not used in AES algorithm."
+ */
+
+
+/*****************************************************************************/
+/* Private functions: */
+/*****************************************************************************/
+/*
+static uint8_t getSBoxValue(uint8_t num)
+{
+ return sbox[num];
+}
+*/
+#define getSBoxValue(num) (sbox[(num)])
+
+// This function produces Nb(Nr+1) round keys. The round keys are used in each round to decrypt the states.
+static void KeyExpansion(uint8_t* RoundKey, const uint8_t* Key)
+{
+ unsigned i, j, k;
+ uint8_t tempa[4]; // Used for the column/row operations
+
+ // The first round key is the key itself.
+ for (i = 0; i < Nk; ++i)
+ {
+ RoundKey[(i * 4) + 0] = Key[(i * 4) + 0];
+ RoundKey[(i * 4) + 1] = Key[(i * 4) + 1];
+ RoundKey[(i * 4) + 2] = Key[(i * 4) + 2];
+ RoundKey[(i * 4) + 3] = Key[(i * 4) + 3];
+ }
+
+ // All other round keys are found from the previous round keys.
+ for (i = Nk; i < Nb * (Nr + 1); ++i)
+ {
+ {
+ k = (i - 1) * 4;
+ tempa[0]=RoundKey[k + 0];
+ tempa[1]=RoundKey[k + 1];
+ tempa[2]=RoundKey[k + 2];
+ tempa[3]=RoundKey[k + 3];
+
+ }
+
+ if (i % Nk == 0)
+ {
+ // This function shifts the 4 bytes in a word to the left once.
+ // [a0,a1,a2,a3] becomes [a1,a2,a3,a0]
+
+ // Function RotWord()
+ {
+ const uint8_t u8tmp = tempa[0];
+ tempa[0] = tempa[1];
+ tempa[1] = tempa[2];
+ tempa[2] = tempa[3];
+ tempa[3] = u8tmp;
+ }
+
+ // SubWord() is a function that takes a four-byte input word and
+ // applies the S-box to each of the four bytes to produce an output word.
+
+ // Function Subword()
+ {
+ tempa[0] = getSBoxValue(tempa[0]);
+ tempa[1] = getSBoxValue(tempa[1]);
+ tempa[2] = getSBoxValue(tempa[2]);
+ tempa[3] = getSBoxValue(tempa[3]);
+ }
+
+ tempa[0] = tempa[0] ^ Rcon[i/Nk];
+ }
+#if defined(AES256) && (AES256 == 1)
+ if (i % Nk == 4)
+ {
+ // Function Subword()
+ {
+ tempa[0] = getSBoxValue(tempa[0]);
+ tempa[1] = getSBoxValue(tempa[1]);
+ tempa[2] = getSBoxValue(tempa[2]);
+ tempa[3] = getSBoxValue(tempa[3]);
+ }
+ }
+#endif
+ j = i * 4; k=(i - Nk) * 4;
+ RoundKey[j + 0] = RoundKey[k + 0] ^ tempa[0];
+ RoundKey[j + 1] = RoundKey[k + 1] ^ tempa[1];
+ RoundKey[j + 2] = RoundKey[k + 2] ^ tempa[2];
+ RoundKey[j + 3] = RoundKey[k + 3] ^ tempa[3];
+ }
+}
+
+void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key)
+{
+ KeyExpansion(ctx->RoundKey, key);
+}
+#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
+void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv)
+{
+ KeyExpansion(ctx->RoundKey, key);
+ memcpy (ctx->Iv, iv, AES_BLOCKLEN);
+}
+void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv)
+{
+ memcpy (ctx->Iv, iv, AES_BLOCKLEN);
+}
+#endif
+
+// This function adds the round key to state.
+// The round key is added to the state by an XOR function.
+static void AddRoundKey(uint8_t round, state_t* state, const uint8_t* RoundKey)
+{
+ uint8_t i,j;
+ for (i = 0; i < 4; ++i)
+ {
+ for (j = 0; j < 4; ++j)
+ {
+ (*state)[i][j] ^= RoundKey[(round * Nb * 4) + (i * Nb) + j];
+ }
+ }
+}
+
+// The SubBytes Function Substitutes the values in the
+// state matrix with values in an S-box.
+static void SubBytes(state_t* state)
+{
+ uint8_t i, j;
+ for (i = 0; i < 4; ++i)
+ {
+ for (j = 0; j < 4; ++j)
+ {
+ (*state)[j][i] = getSBoxValue((*state)[j][i]);
+ }
+ }
+}
+
+// The ShiftRows() function shifts the rows in the state to the left.
+// Each row is shifted with different offset.
+// Offset = Row number. So the first row is not shifted.
+static void ShiftRows(state_t* state)
+{
+ uint8_t temp;
+
+ // Rotate first row 1 columns to left
+ temp = (*state)[0][1];
+ (*state)[0][1] = (*state)[1][1];
+ (*state)[1][1] = (*state)[2][1];
+ (*state)[2][1] = (*state)[3][1];
+ (*state)[3][1] = temp;
+
+ // Rotate second row 2 columns to left
+ temp = (*state)[0][2];
+ (*state)[0][2] = (*state)[2][2];
+ (*state)[2][2] = temp;
+
+ temp = (*state)[1][2];
+ (*state)[1][2] = (*state)[3][2];
+ (*state)[3][2] = temp;
+
+ // Rotate third row 3 columns to left
+ temp = (*state)[0][3];
+ (*state)[0][3] = (*state)[3][3];
+ (*state)[3][3] = (*state)[2][3];
+ (*state)[2][3] = (*state)[1][3];
+ (*state)[1][3] = temp;
+}
+
+static uint8_t xtime(uint8_t x)
+{
+ return ((x<<1) ^ (((x>>7) & 1) * 0x1b));
+}
+
+// MixColumns function mixes the columns of the state matrix
+static void MixColumns(state_t* state)
+{
+ uint8_t i;
+ uint8_t Tmp, Tm, t;
+ for (i = 0; i < 4; ++i)
+ {
+ t = (*state)[i][0];
+ Tmp = (*state)[i][0] ^ (*state)[i][1] ^ (*state)[i][2] ^ (*state)[i][3] ;
+ Tm = (*state)[i][0] ^ (*state)[i][1] ; Tm = xtime(Tm); (*state)[i][0] ^= Tm ^ Tmp ;
+ Tm = (*state)[i][1] ^ (*state)[i][2] ; Tm = xtime(Tm); (*state)[i][1] ^= Tm ^ Tmp ;
+ Tm = (*state)[i][2] ^ (*state)[i][3] ; Tm = xtime(Tm); (*state)[i][2] ^= Tm ^ Tmp ;
+ Tm = (*state)[i][3] ^ t ; Tm = xtime(Tm); (*state)[i][3] ^= Tm ^ Tmp ;
+ }
+}
+
+// Multiply is used to multiply numbers in the field GF(2^8)
+// Note: The last call to xtime() is unneeded, but often ends up generating a smaller binary
+// The compiler seems to be able to vectorize the operation better this way.
+// See https://github.com/kokke/tiny-AES-c/pull/34
+#if MULTIPLY_AS_A_FUNCTION
+static uint8_t Multiply(uint8_t x, uint8_t y)
+{
+ return (((y & 1) * x) ^
+ ((y>>1 & 1) * xtime(x)) ^
+ ((y>>2 & 1) * xtime(xtime(x))) ^
+ ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^
+ ((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))); /* this last call to xtime() can be omitted */
+ }
+#else
+#define Multiply(x, y) \
+ ( ((y & 1) * x) ^ \
+ ((y>>1 & 1) * xtime(x)) ^ \
+ ((y>>2 & 1) * xtime(xtime(x))) ^ \
+ ((y>>3 & 1) * xtime(xtime(xtime(x)))) ^ \
+ ((y>>4 & 1) * xtime(xtime(xtime(xtime(x)))))) \
+
+#endif
+
+#if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1)
+/*
+static uint8_t getSBoxInvert(uint8_t num)
+{
+ return rsbox[num];
+}
+*/
+#define getSBoxInvert(num) (rsbox[(num)])
+
+// MixColumns function mixes the columns of the state matrix.
+// The method used to multiply may be difficult to understand for the inexperienced.
+// Please use the references to gain more information.
+static void InvMixColumns(state_t* state)
+{
+ int i;
+ uint8_t a, b, c, d;
+ for (i = 0; i < 4; ++i)
+ {
+ a = (*state)[i][0];
+ b = (*state)[i][1];
+ c = (*state)[i][2];
+ d = (*state)[i][3];
+
+ (*state)[i][0] = Multiply(a, 0x0e) ^ Multiply(b, 0x0b) ^ Multiply(c, 0x0d) ^ Multiply(d, 0x09);
+ (*state)[i][1] = Multiply(a, 0x09) ^ Multiply(b, 0x0e) ^ Multiply(c, 0x0b) ^ Multiply(d, 0x0d);
+ (*state)[i][2] = Multiply(a, 0x0d) ^ Multiply(b, 0x09) ^ Multiply(c, 0x0e) ^ Multiply(d, 0x0b);
+ (*state)[i][3] = Multiply(a, 0x0b) ^ Multiply(b, 0x0d) ^ Multiply(c, 0x09) ^ Multiply(d, 0x0e);
+ }
+}
+
+
+// The SubBytes Function Substitutes the values in the
+// state matrix with values in an S-box.
+static void InvSubBytes(state_t* state)
+{
+ uint8_t i, j;
+ for (i = 0; i < 4; ++i)
+ {
+ for (j = 0; j < 4; ++j)
+ {
+ (*state)[j][i] = getSBoxInvert((*state)[j][i]);
+ }
+ }
+}
+
+static void InvShiftRows(state_t* state)
+{
+ uint8_t temp;
+
+ // Rotate first row 1 columns to right
+ temp = (*state)[3][1];
+ (*state)[3][1] = (*state)[2][1];
+ (*state)[2][1] = (*state)[1][1];
+ (*state)[1][1] = (*state)[0][1];
+ (*state)[0][1] = temp;
+
+ // Rotate second row 2 columns to right
+ temp = (*state)[0][2];
+ (*state)[0][2] = (*state)[2][2];
+ (*state)[2][2] = temp;
+
+ temp = (*state)[1][2];
+ (*state)[1][2] = (*state)[3][2];
+ (*state)[3][2] = temp;
+
+ // Rotate third row 3 columns to right
+ temp = (*state)[0][3];
+ (*state)[0][3] = (*state)[1][3];
+ (*state)[1][3] = (*state)[2][3];
+ (*state)[2][3] = (*state)[3][3];
+ (*state)[3][3] = temp;
+}
+#endif // #if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1)
+
+// Cipher is the main function that encrypts the PlainText.
+static void Cipher(state_t* state, const uint8_t* RoundKey)
+{
+ uint8_t round = 0;
+
+ // Add the First round key to the state before starting the rounds.
+ AddRoundKey(0, state, RoundKey);
+
+ // There will be Nr rounds.
+ // The first Nr-1 rounds are identical.
+ // These Nr rounds are executed in the loop below.
+ // Last one without MixColumns()
+ for (round = 1; ; ++round)
+ {
+ SubBytes(state);
+ ShiftRows(state);
+ if (round == Nr) {
+ break;
+ }
+ MixColumns(state);
+ AddRoundKey(round, state, RoundKey);
+ }
+ // Add round key to last round
+ AddRoundKey(Nr, state, RoundKey);
+}
+
+#if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1)
+static void InvCipher(state_t* state, const uint8_t* RoundKey)
+{
+ uint8_t round = 0;
+
+ // Add the First round key to the state before starting the rounds.
+ AddRoundKey(Nr, state, RoundKey);
+
+ // There will be Nr rounds.
+ // The first Nr-1 rounds are identical.
+ // These Nr rounds are executed in the loop below.
+ // Last one without InvMixColumn()
+ for (round = (Nr - 1); ; --round)
+ {
+ InvShiftRows(state);
+ InvSubBytes(state);
+ AddRoundKey(round, state, RoundKey);
+ if (round == 0) {
+ break;
+ }
+ InvMixColumns(state);
+ }
+
+}
+#endif // #if (defined(CBC) && CBC == 1) || (defined(ECB) && ECB == 1)
+
+/*****************************************************************************/
+/* Public functions: */
+/*****************************************************************************/
+#if defined(ECB) && (ECB == 1)
+
+
+void AES_ECB_encrypt(const struct AES_ctx* ctx, uint8_t* buf)
+{
+ // The next function call encrypts the PlainText with the Key using AES algorithm.
+ Cipher((state_t*)buf, ctx->RoundKey);
+}
+
+void AES_ECB_decrypt(const struct AES_ctx* ctx, uint8_t* buf)
+{
+ // The next function call decrypts the PlainText with the Key using AES algorithm.
+ InvCipher((state_t*)buf, ctx->RoundKey);
+}
+
+
+#endif // #if defined(ECB) && (ECB == 1)
+
+
+
+
+
+#if defined(CBC) && (CBC == 1)
+
+
+static void XorWithIv(uint8_t* buf, const uint8_t* Iv)
+{
+ uint8_t i;
+ for (i = 0; i < AES_BLOCKLEN; ++i) // The block in AES is always 128bit no matter the key size
+ {
+ buf[i] ^= Iv[i];
+ }
+}
+
+void AES_CBC_encrypt_buffer(struct AES_ctx *ctx, uint8_t* buf, size_t length)
+{
+ size_t i;
+ uint8_t *Iv = ctx->Iv;
+ for (i = 0; i < length; i += AES_BLOCKLEN)
+ {
+ XorWithIv(buf, Iv);
+ Cipher((state_t*)buf, ctx->RoundKey);
+ Iv = buf;
+ buf += AES_BLOCKLEN;
+ }
+ /* store Iv in ctx for next call */
+ memcpy(ctx->Iv, Iv, AES_BLOCKLEN);
+}
+
+void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
+{
+ size_t i;
+ uint8_t storeNextIv[AES_BLOCKLEN];
+ for (i = 0; i < length; i += AES_BLOCKLEN)
+ {
+ memcpy(storeNextIv, buf, AES_BLOCKLEN);
+ InvCipher((state_t*)buf, ctx->RoundKey);
+ XorWithIv(buf, ctx->Iv);
+ memcpy(ctx->Iv, storeNextIv, AES_BLOCKLEN);
+ buf += AES_BLOCKLEN;
+ }
+
+}
+
+#endif // #if defined(CBC) && (CBC == 1)
+
+
+
+#if defined(CTR) && (CTR == 1)
+
+/* Symmetrical operation: same function for encrypting as for decrypting. Note any IV/nonce should never be reused with the same key */
+void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length)
+{
+ uint8_t buffer[AES_BLOCKLEN];
+
+ size_t i;
+ int bi;
+ for (i = 0, bi = AES_BLOCKLEN; i < length; ++i, ++bi)
+ {
+ if (bi == AES_BLOCKLEN) /* we need to regen xor compliment in buffer */
+ {
+
+ memcpy(buffer, ctx->Iv, AES_BLOCKLEN);
+ Cipher((state_t*)buffer,ctx->RoundKey);
+
+ /* Increment Iv and handle overflow */
+ for (bi = (AES_BLOCKLEN - 1); bi >= 0; --bi)
+ {
+ /* inc will overflow */
+ if (ctx->Iv[bi] == 255)
+ {
+ ctx->Iv[bi] = 0;
+ continue;
+ }
+ ctx->Iv[bi] += 1;
+ break;
+ }
+ bi = 0;
+ }
+
+ buf[i] = (buf[i] ^ buffer[bi]);
+ }
+}
+
+#endif // #if defined(CTR) && (CTR == 1)
diff --git a/src/plugins/utils/aes.h b/src/plugins/utils/aes.h
new file mode 100644
index 0000000..dcf8521
--- /dev/null
+++ b/src/plugins/utils/aes.h
@@ -0,0 +1,91 @@
+#ifndef _AES_H_
+#define _AES_H_
+
+#include <stdint.h>
+#include <stddef.h>
+
+// #define the macros below to 1/0 to enable/disable the mode of operation.
+//
+// CBC enables AES encryption in CBC-mode of operation.
+// CTR enables encryption in counter-mode.
+// ECB enables the basic ECB 16-byte block algorithm. All can be enabled simultaneously.
+
+// The #ifndef-guard allows it to be configured before #include'ing or at compile time.
+#ifndef CBC
+ #define CBC 1
+#endif
+
+// #ifndef ECB
+// #define ECB 1
+// #endif
+
+// #ifndef CTR
+// #define CTR 1
+// #endif
+
+
+//#define AES128 1
+//#define AES192 1
+#define AES256 1
+
+#define AES_BLOCKLEN 16 // Block length in bytes - AES is 128b block only
+
+#if defined(AES256) && (AES256 == 1)
+ #define AES_KEYLEN 32
+ #define AES_keyExpSize 240
+#elif defined(AES192) && (AES192 == 1)
+ #define AES_KEYLEN 24
+ #define AES_keyExpSize 208
+#else
+ #define AES_KEYLEN 16 // Key length in bytes
+ #define AES_keyExpSize 176
+#endif
+
+struct AES_ctx
+{
+ uint8_t RoundKey[AES_keyExpSize];
+#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
+ uint8_t Iv[AES_BLOCKLEN];
+#endif
+};
+
+void AES_init_ctx(struct AES_ctx* ctx, const uint8_t* key);
+#if (defined(CBC) && (CBC == 1)) || (defined(CTR) && (CTR == 1))
+void AES_init_ctx_iv(struct AES_ctx* ctx, const uint8_t* key, const uint8_t* iv);
+void AES_ctx_set_iv(struct AES_ctx* ctx, const uint8_t* iv);
+#endif
+
+#if defined(ECB) && (ECB == 1)
+// buffer size is exactly AES_BLOCKLEN bytes;
+// you need only AES_init_ctx as IV is not used in ECB
+// NB: ECB is considered insecure for most uses
+void AES_ECB_encrypt(const struct AES_ctx* ctx, uint8_t* buf);
+void AES_ECB_decrypt(const struct AES_ctx* ctx, uint8_t* buf);
+
+#endif // #if defined(ECB) && (ECB == !)
+
+
+#if defined(CBC) && (CBC == 1)
+// buffer size MUST be mutile of AES_BLOCKLEN;
+// Suggest https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme
+// NOTES: you need to set IV in ctx via AES_init_ctx_iv() or AES_ctx_set_iv()
+// no IV should ever be reused with the same key
+void AES_CBC_encrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
+void AES_CBC_decrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
+
+#endif // #if defined(CBC) && (CBC == 1)
+
+
+#if defined(CTR) && (CTR == 1)
+
+// Same function for encrypting as for decrypting.
+// IV is incremented for every block, and used after encryption as XOR-compliment for output
+// Suggesting https://en.wikipedia.org/wiki/Padding_(cryptography)#PKCS7 for padding scheme
+// NOTES: you need to set IV in ctx with AES_init_ctx_iv() or AES_ctx_set_iv()
+// no IV should ever be reused with the same key
+void AES_CTR_xcrypt_buffer(struct AES_ctx* ctx, uint8_t* buf, size_t length);
+
+#endif // #if defined(CTR) && (CTR == 1)
+
+
+#endif // _AES_H_ \ No newline at end of file