From 89383cff1ba5d8a928262fcb4c40382a981c78c8 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Fri, 11 Jun 2021 04:51:03 +0200 Subject: Remove dependency on youtube-dl for streaming youtube, resulting in faster video startup --- src/AsyncImageLoader.cpp | 17 +-- src/DownloadUtils.cpp | 4 + src/Program.cpp | 4 + src/QuickMedia.cpp | 229 ++++++++++++++++------------ src/StringUtils.cpp | 16 +- src/VideoPlayer.cpp | 14 +- src/plugins/ImageBoard.cpp | 2 +- src/plugins/MediaGeneric.cpp | 2 +- src/plugins/Youtube.cpp | 269 ++++++++++++++++++++++++++++++++- src/plugins/youtube/Signature.cpp | 307 ++++++++++++++++++++++++++++++++++++++ 10 files changed, 740 insertions(+), 124 deletions(-) create mode 100644 src/plugins/youtube/Signature.cpp (limited to 'src') diff --git a/src/AsyncImageLoader.cpp b/src/AsyncImageLoader.cpp index 1a1e120..9e8d090 100644 --- a/src/AsyncImageLoader.cpp +++ b/src/AsyncImageLoader.cpp @@ -87,7 +87,7 @@ namespace QuickMedia { loading_image[i] = false; } - load_image_thread = std::thread([this]() mutable { + load_image_thread = AsyncTask([this]() mutable { std::optional thumbnail_load_data_opt; while(true) { thumbnail_load_data_opt = image_load_queue.pop_wait(); @@ -126,17 +126,6 @@ namespace QuickMedia { AsyncImageLoader::~AsyncImageLoader() { image_load_queue.close(); - if(load_image_thread.joinable()) { - program_kill_in_thread(load_image_thread.get_id()); - load_image_thread.join(); - } - - for(size_t i = 0; i < NUM_IMAGE_LOAD_THREADS; ++i) { - if(download_image_thread[i].joinable()) { - program_kill_in_thread(download_image_thread[i].get_id()); - download_image_thread[i].join(); - } - } } void AsyncImageLoader::load_thumbnail(const std::string &url, bool local, sf::Vector2i resize_target_size, std::shared_ptr thumbnail_data) { @@ -178,11 +167,9 @@ namespace QuickMedia { loading_image[free_index] = true; thumbnail_data->loading_state = LoadingState::LOADING; - if(download_image_thread[free_index].joinable()) - download_image_thread[free_index].join(); // TODO: Keep the thread running and use conditional variable instead to sleep until a new image should be loaded. Same in ImageViewer. - download_image_thread[free_index] = std::thread([this, free_index, thumbnail_path, url, resize_target_size, thumbnail_data]() mutable { + download_image_thread[free_index] = AsyncTask([this, free_index, thumbnail_path, url, resize_target_size, thumbnail_data]() mutable { thumbnail_data->image = std::make_unique(); Path thumbnail_path_resized = thumbnail_path; diff --git a/src/DownloadUtils.cpp b/src/DownloadUtils.cpp index 774fba9..0977b78 100644 --- a/src/DownloadUtils.cpp +++ b/src/DownloadUtils.cpp @@ -24,6 +24,7 @@ static const char *useragent_str = "user-agent: Mozilla/5.0 (X11; Linux x86_64) namespace QuickMedia { DownloadResult download_head_to_string(const std::string &url, std::string &result, bool use_browser_useragent, bool fail_on_error) { + result.clear(); sf::Clock timer; std::vector args; args.insert(args.end(), { "curl", "-I", "-g", "-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", "--compressed", "-s" }); @@ -50,6 +51,7 @@ namespace QuickMedia { } DownloadResult url_get_remote_name(const std::string &url, std::string &result, bool use_browser_useragent) { + result.clear(); sf::Clock timer; std::vector args; args.insert(args.end(), { "curl", "-I", "-g", "-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", "--compressed", "-s" }); @@ -131,6 +133,7 @@ namespace QuickMedia { // TODO: Add timeout DownloadResult download_to_string(const std::string &url, std::string &result, const std::vector &additional_args, bool use_browser_useragent, bool fail_on_error) { + result.clear(); sf::Clock timer; std::vector args; args.insert(args.end(), { "curl", "-H", "Accept-Language: en-US,en;q=0.5", "-H", "Connection: keep-alive", "--compressed", "-g", "-s", "-L" }); @@ -162,6 +165,7 @@ namespace QuickMedia { } DownloadResult download_to_string_cache(const std::string &url, std::string &result, const std::vector &additional_args, bool use_browser_useragent, DownloadErrorHandler error_handler, Path cache_path) { + result.clear(); Path media_file_path; if(cache_path.data.empty()) { SHA256 sha256; diff --git a/src/Program.cpp b/src/Program.cpp index a8189ae..5220a4c 100644 --- a/src/Program.cpp +++ b/src/Program.cpp @@ -280,3 +280,7 @@ void program_clear_current_thread() { void program_kill_in_thread(const std::thread::id &thread_id) { current_thread_program.kill_in_thread(thread_id); } + +bool program_is_dead_in_current_thread() { + return current_thread_program.is_killed(); +} diff --git a/src/QuickMedia.cpp b/src/QuickMedia.cpp index fa0f0d8..7479299 100644 --- a/src/QuickMedia.cpp +++ b/src/QuickMedia.cpp @@ -2145,30 +2145,6 @@ namespace QuickMedia { return false; } - static bool youtube_url_extract_id(const std::string &youtube_url, std::string &youtube_video_id) { - size_t index = youtube_url.find("youtube.com/watch?v="); - if(index != std::string::npos) { - index += 20; - size_t end_index = youtube_url.find("&", index); - if(end_index == std::string::npos) - end_index = youtube_url.size(); - youtube_video_id = youtube_url.substr(index, end_index - index); - return true; - } - - index = youtube_url.find("youtu.be/"); - if(index != std::string::npos) { - index += 9; - size_t end_index = youtube_url.find("?", index); - if(end_index == std::string::npos) - end_index = youtube_url.size(); - youtube_video_id = youtube_url.substr(index, end_index - index); - return true; - } - - return false; - } - static int watch_history_get_item_by_id(const Json::Value &video_history_json, const char *id) { assert(video_history_json.isArray()); @@ -2372,68 +2348,14 @@ namespace QuickMedia { void Program::video_content_page(Page *parent_page, VideoPage *video_page, std::string video_title, bool download_if_streaming_fails, BodyItems &next_play_items, int play_index, int *parent_body_page, const std::string &parent_page_search) { PageType previous_page = pop_page_stack(); - std::string video_url = video_page->get_url(); - std::string original_video_url = video_url; sf::Clock time_watched_timer; bool video_loaded = false; - std::string youtube_video_id; - const bool is_youtube = youtube_url_extract_id(video_url, youtube_video_id); + std::string youtube_video_id_dummy; + const bool is_youtube = youtube_url_extract_id(video_page->get_url(), youtube_video_id_dummy); const bool is_matrix = strcmp(plugin_name, "matrix") == 0; - bool video_url_is_local = false; - if(!is_youtube && download_if_streaming_fails) { - Path video_cache_dir = get_cache_dir().join("media"); - Path video_path = video_cache_dir; - SHA256 sha256; - sha256.add(video_url.data(), video_url.size()); - video_path.join(sha256.getHash()); - if(get_file_type(video_path) == FileType::REGULAR) { - fprintf(stderr, "%s is found in cache. Playing from cache...\n", video_url.c_str()); - video_url = std::move(video_path.data); - video_url_is_local = true; - } else { - TaskResult video_is_not_streamble_result = run_task_with_loading_screen([video_url]() { - return video_url_is_non_streamable_mp4(video_url.c_str()); - }); - if(video_is_not_streamble_result == TaskResult::TRUE) { - fprintf(stderr, "%s is detected to be a non-streamable mp4 file, downloading it before playing it...\n", video_url.c_str()); - if(create_directory_recursive(video_cache_dir) != 0) { - show_notification("QuickMedia", "Failed to create video cache directory", Urgency::CRITICAL); - current_page = previous_page; - return; - } - - TaskResult download_file_result = run_task_with_loading_screen([&video_path, video_url]() { - return download_to_file(video_url, video_path.data, {}, true) == DownloadResult::OK; - }); - switch(download_file_result) { - case TaskResult::TRUE: { - video_url = std::move(video_path.data); - video_url_is_local = true; - break; - } - case TaskResult::FALSE: { - show_notification("QuickMedia", "Failed to download " + video_url, Urgency::CRITICAL); - current_page = previous_page; - return; - } - case TaskResult::CANCEL: { - current_page = previous_page; - return; - } - } - } else if(video_is_not_streamble_result == TaskResult::CANCEL) { - current_page = previous_page; - return; - } - } - } - - window.setFramerateLimit(FPS_IDLE); - idle = true; - - time_watched_timer.restart(); + idle_active_handler(); std::unique_ptr video_player; BodyItems related_videos; @@ -2451,14 +2373,110 @@ namespace QuickMedia { std::function video_event_callback; - auto load_video_error_check = [this, &related_videos, &channel_url, &video_url, &video_title, &video_player, previous_page, &time_watched_timer, &video_loaded, video_page, &video_event_callback, &on_window_create, &video_player_window, is_youtube, is_matrix](bool resume_video) mutable { + auto load_video_error_check = [this, &related_videos, &channel_url, &video_title, &video_player, previous_page, &time_watched_timer, &video_loaded, video_page, &video_event_callback, &on_window_create, &video_player_window, is_youtube, is_matrix, download_if_streaming_fails](bool resume_video) mutable { + TaskResult load_result = run_task_with_loading_screen([video_page]() { + return video_page->load() == PluginResult::OK; + }); + + if(load_result == TaskResult::CANCEL) { + current_page = previous_page; + return; + } else if(load_result == TaskResult::FALSE) { + show_notification("QuickMedia", "Failed to load media", Urgency::CRITICAL); + current_page = previous_page; + return; + } + + bool is_audio_only = no_video; + bool has_embedded_audio = true; + const int largest_monitor_height = get_largest_monitor_height(disp); + std::string video_url = video_page->get_video_url(largest_monitor_height, has_embedded_audio); + std::string audio_url; + if(video_url.empty() || no_video) { + video_url = video_page->get_audio_url(); + if(video_url.empty()) { + video_url = video_page->get_url(); + has_embedded_audio = true; + } else { + is_audio_only = true; + has_embedded_audio = false; + } + } else if(!has_embedded_audio) { + audio_url = video_page->get_audio_url(); + } + + bool video_url_is_local = false; + if(!is_youtube && download_if_streaming_fails) { + Path video_cache_dir = get_cache_dir().join("media"); + Path video_path = video_cache_dir; + SHA256 sha256; + sha256.add(video_url.data(), video_url.size()); + video_path.join(sha256.getHash()); + if(get_file_type(video_path) == FileType::REGULAR) { + fprintf(stderr, "%s is found in cache. Playing from cache...\n", video_url.c_str()); + video_url = std::move(video_path.data); + video_url_is_local = true; + audio_url.clear(); + if(no_video) { + is_audio_only = true; + has_embedded_audio = false; + } else { + is_audio_only = false; + has_embedded_audio = true; + } + } else { + TaskResult video_is_not_streamble_result = run_task_with_loading_screen([video_url]() { + return video_url_is_non_streamable_mp4(video_url.c_str()); + }); + if(video_is_not_streamble_result == TaskResult::TRUE) { + fprintf(stderr, "%s is detected to be a non-streamable mp4 file, downloading it before playing it...\n", video_url.c_str()); + if(create_directory_recursive(video_cache_dir) != 0) { + show_notification("QuickMedia", "Failed to create video cache directory", Urgency::CRITICAL); + current_page = previous_page; + return; + } + + TaskResult download_file_result = run_task_with_loading_screen([&video_path, video_url]() { + return download_to_file(video_url, video_path.data, {}, true) == DownloadResult::OK; + }); + switch(download_file_result) { + case TaskResult::TRUE: { + video_url = std::move(video_path.data); + video_url_is_local = true; + audio_url.clear(); + if(no_video) { + is_audio_only = true; + has_embedded_audio = false; + } else { + is_audio_only = false; + has_embedded_audio = true; + } + break; + } + case TaskResult::FALSE: { + show_notification("QuickMedia", "Failed to download " + video_url, Urgency::CRITICAL); + current_page = previous_page; + return; + } + case TaskResult::CANCEL: { + current_page = previous_page; + return; + } + } + } else if(video_is_not_streamble_result == TaskResult::CANCEL) { + current_page = previous_page; + return; + } + } + } + time_watched_timer.restart(); video_loaded = false; video_player_window = None; watched_videos.insert(video_url); - video_player = std::make_unique(no_video, use_system_mpv_config, resume_video, is_matrix && !is_youtube, video_event_callback, on_window_create, resources_root, get_largest_monitor_height(disp)); - VideoPlayer::Error err = video_player->load_video(video_url.c_str(), window.getSystemHandle(), plugin_name, video_title); + video_player = std::make_unique(is_audio_only, use_system_mpv_config, resume_video, is_matrix && !is_youtube, video_event_callback, on_window_create, resources_root, largest_monitor_height); + VideoPlayer::Error err = video_player->load_video(video_url.c_str(), audio_url.c_str(), window.getSystemHandle(), plugin_name, video_title); if(err != VideoPlayer::Error::OK) { std::string err_msg = "Failed to play url: "; err_msg += video_url; @@ -2469,8 +2487,9 @@ namespace QuickMedia { return; channel_url.clear(); - TaskResult load_related_media_result = run_task_with_loading_screen([video_page, &related_videos, &video_url, &channel_url]{ - related_videos = video_page->get_related_media(video_url, channel_url); + TaskResult load_related_media_result = run_task_with_loading_screen([video_page, &related_videos, &channel_url]{ + // TODO: Do async + related_videos = video_page->get_related_media(video_page->get_url(), channel_url); return true; }); @@ -2484,9 +2503,9 @@ namespace QuickMedia { return; std::string video_id; - if(!youtube_url_extract_id(video_url, video_id)) { + if(!youtube_url_extract_id(video_page->get_url(), video_id)) { std::string err_msg = "Failed to extract id of youtube url "; - err_msg += video_url; + err_msg += video_page->get_url(); err_msg + ", video wont be saved in history"; show_notification("QuickMedia", err_msg.c_str(), Urgency::LOW); return; @@ -2545,20 +2564,21 @@ namespace QuickMedia { bool cursor_visible = true; sf::Clock cursor_hide_timer; - auto save_video_url_to_clipboard = [&original_video_url, &video_player]() { - if(video_url_supports_timestamp(original_video_url)) { + auto save_video_url_to_clipboard = [video_page, &video_player]() { + std::string url = video_page->get_url(); + if(video_url_supports_timestamp(url)) { // TODO: Remove timestamp (&t= or ?t=) from video_url double time_in_file = 0.0; if(video_player && (video_player->get_time_in_file(&time_in_file) != VideoPlayer::Error::OK)) time_in_file = 0.0; - std::string clipboard = original_video_url; + std::string clipboard = std::move(url); if((int)time_in_file > 0) clipboard += "&t=" + std::to_string((int)time_in_file); sf::Clipboard::setString(sf::String::fromUtf8(clipboard.begin(), clipboard.end())); } else { - sf::Clipboard::setString(sf::String::fromUtf8(original_video_url.begin(), original_video_url.end())); + sf::Clipboard::setString(sf::String::fromUtf8(url.begin(), url.end())); } }; @@ -2600,7 +2620,7 @@ namespace QuickMedia { } else if(pressed_keysym == XK_f && pressing_ctrl) { window_set_fullscreen(disp, window.getSystemHandle(), WindowFullscreenState::TOGGLE); } else if(pressed_keysym == XK_s && pressing_ctrl) { - video_page_download_video(original_video_url, !is_matrix || is_youtube, video_player_window); + video_page_download_video(video_page->get_url(), !is_matrix || is_youtube, video_player_window); } else if(pressed_keysym == XK_r && pressing_ctrl) { if(!cursor_visible) window.setMouseCursorVisible(true); @@ -2609,7 +2629,7 @@ namespace QuickMedia { int search_delay = 0; auto search_page = video_page->create_search_page(this, search_delay); auto comments_page = video_page->create_comments_page(this); - auto related_videos_page = video_page->create_related_videos_page(this, video_url, video_title); + auto related_videos_page = video_page->create_related_videos_page(this); auto channels_page = video_page->create_channels_page(this, channel_url); if(search_page || related_videos_page || channels_page) { XUnmapWindow(disp, video_player_window); @@ -2654,7 +2674,6 @@ namespace QuickMedia { if(page_changed) { current_page = PageType::VIDEO_CONTENT; - //video_player = std::make_unique(no_video, use_system_mpv_config, true, video_event_callback, on_window_create, resources_root); load_video_error_check(true); } else { XMapWindow(disp, video_player_window); @@ -2729,8 +2748,20 @@ namespace QuickMedia { break; } - video_url = video_page->url_get_playable_url(new_video_url); - original_video_url = video_url; + TaskResult get_playable_url_result = run_task_with_loading_screen([video_page, &new_video_url]() { + video_page->set_url(video_page->url_get_playable_url(new_video_url)); + return true; + }); + + if(get_playable_url_result == TaskResult::CANCEL) { + current_page = previous_page; + return; + } else if(get_playable_url_result == TaskResult::FALSE) { + show_notification("QuickMedia", "Failed to get playable url", Urgency::CRITICAL); + current_page = previous_page; + return; + } + video_title = std::move(new_video_title); load_video_error_check(false); } else if(update_err != VideoPlayer::Error::OK) { @@ -3516,7 +3547,7 @@ namespace QuickMedia { page_stack.push(PageType::IMAGE_BOARD_THREAD); current_page = PageType::VIDEO_CONTENT; watched_videos.clear(); - thread_page->video_url = selected_item->url; + thread_page->set_url(selected_item->url); BodyItems next_items; // TODO: Use real title video_content_page(thread_page, thread_page, selected_item->get_title(), true, thread_body->items, thread_body->get_selected_item()); @@ -5054,7 +5085,7 @@ namespace QuickMedia { bool is_audio = (message_type == MessageType::AUDIO); bool prev_no_video = no_video; no_video = is_audio; - video_page->url = selected->url; + video_page->set_url(selected->url); BodyItems next_items; // TODO: Add title video_content_page(matrix_chat_page, video_page.get(), "", message_type == MessageType::VIDEO || message_type == MessageType::AUDIO, next_items, 0); diff --git a/src/StringUtils.cpp b/src/StringUtils.cpp index 0345558..8ed142f 100644 --- a/src/StringUtils.cpp +++ b/src/StringUtils.cpp @@ -2,7 +2,8 @@ #include namespace QuickMedia { - void string_split(const std::string &str, char delimiter, StringSplitCallback callback_func) { + template + static void string_split_t(const std::string &str, const T &delimiter, StringSplitCallback callback_func) { size_t index = 0; while(index < str.size()) { size_t new_index = str.find(delimiter, index); @@ -12,10 +13,21 @@ namespace QuickMedia { if(!callback_func(str.data() + index, new_index - index)) break; - index = new_index + 1; + if constexpr(std::is_same::value) + index = new_index + 1; + else + index = new_index + delimiter.size(); } } + void string_split(const std::string &str, const std::string &delimiter, StringSplitCallback callback_func) { + string_split_t(str, delimiter, callback_func); + } + + void string_split(const std::string &str, char delimiter, StringSplitCallback callback_func) { + string_split_t(str, delimiter, callback_func); + } + size_t string_replace_all(std::string &str, char old_char, char new_char) { size_t num_replaced_substrings = 0; for(char &c : str) { diff --git a/src/VideoPlayer.cpp b/src/VideoPlayer.cpp index adebddc..2b0653c 100644 --- a/src/VideoPlayer.cpp +++ b/src/VideoPlayer.cpp @@ -80,7 +80,7 @@ namespace QuickMedia { return result; } - VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, sf::WindowHandle _parent_window, const std::string &plugin_name, const std::string &) { + VideoPlayer::Error VideoPlayer::launch_video_process(const char *path, const char *audio_path, sf::WindowHandle _parent_window, const std::string &plugin_name, const std::string &) { parent_window = _parent_window; if(socketpair(AF_UNIX, SOCK_STREAM, 0, sockets) == -1) { @@ -172,6 +172,12 @@ namespace QuickMedia { if(no_video) args.push_back("--no-video"); + std::string audio_file_arg = "--audio-file="; + if(audio_path && audio_path[0] != '\0') { + audio_file_arg += audio_path; + args.push_back(audio_file_arg.c_str()); + } + args.insert(args.end(), { "--", path, nullptr }); if(exec_program_async(args.data(), &video_process_id) != 0) { @@ -190,12 +196,14 @@ namespace QuickMedia { return Error::OK; } - VideoPlayer::Error VideoPlayer::load_video(const char *path, sf::WindowHandle _parent_window, const std::string &plugin_name, const std::string &title) { + VideoPlayer::Error VideoPlayer::load_video(const char *path, const char *audio_path, sf::WindowHandle _parent_window, const std::string &plugin_name, const std::string &title) { // This check is to make sure we dont change window that the video belongs to. This is not a usecase we will have so // no need to support it for now at least. assert(parent_window == 0 || parent_window == _parent_window); + assert(path); + fprintf(stderr, "Playing video: %s, audio: %s\n", path ? path : "", audio_path ? audio_path : ""); if(video_process_id == -1) - return launch_video_process(path, _parent_window, plugin_name, title); + return launch_video_process(path, audio_path, _parent_window, plugin_name, title); Json::Value command_data(Json::arrayValue); command_data.append("loadfile"); diff --git a/src/plugins/ImageBoard.cpp b/src/plugins/ImageBoard.cpp index 881d8ff..a2ffca7 100644 --- a/src/plugins/ImageBoard.cpp +++ b/src/plugins/ImageBoard.cpp @@ -1,7 +1,7 @@ #include "../../plugins/ImageBoard.hpp" namespace QuickMedia { - std::unique_ptr ImageBoardThreadPage::create_related_videos_page(Program*, const std::string&, const std::string&) { + std::unique_ptr ImageBoardThreadPage::create_related_videos_page(Program*) { return nullptr; } diff --git a/src/plugins/MediaGeneric.cpp b/src/plugins/MediaGeneric.cpp index 19ad87c..c3a8d8e 100644 --- a/src/plugins/MediaGeneric.cpp +++ b/src/plugins/MediaGeneric.cpp @@ -191,7 +191,7 @@ namespace QuickMedia { return std::make_unique(*search_page); } - std::unique_ptr MediaGenericVideoPage::create_related_videos_page(Program *program, const std::string&, const std::string&) { + std::unique_ptr MediaGenericVideoPage::create_related_videos_page(Program *program) { return std::make_unique(program, search_page); } } \ No newline at end of file diff --git a/src/plugins/Youtube.cpp b/src/plugins/Youtube.cpp index d9df409..9cec69c 100644 --- a/src/plugins/Youtube.cpp +++ b/src/plugins/Youtube.cpp @@ -1,10 +1,12 @@ #include "../../plugins/Youtube.hpp" +#include "../../plugins/youtube/Signature.hpp" #include "../../include/Storage.hpp" #include "../../include/NetUtils.hpp" #include "../../include/StringUtils.hpp" #include "../../include/Scale.hpp" #include "../../include/Notification.hpp" #include "../../include/Utils.hpp" +#include extern "C" { #include } @@ -13,6 +15,30 @@ extern "C" { #include namespace QuickMedia { + bool youtube_url_extract_id(const std::string &youtube_url, std::string &youtube_video_id) { + size_t index = youtube_url.find("youtube.com/watch?v="); + if(index != std::string::npos) { + index += 20; + size_t end_index = youtube_url.find("&", index); + if(end_index == std::string::npos) + end_index = youtube_url.size(); + youtube_video_id = youtube_url.substr(index, end_index - index); + return true; + } + + index = youtube_url.find("youtu.be/"); + if(index != std::string::npos) { + index += 9; + size_t end_index = youtube_url.find("?", index); + if(end_index == std::string::npos) + end_index = youtube_url.size(); + youtube_video_id = youtube_url.substr(index, end_index - index); + return true; + } + + return false; + } + // This is a common setup of text in the youtube json static std::optional yt_json_get_text(const Json::Value &json, const char *root_name) { if(!json.isObject()) @@ -257,7 +283,10 @@ namespace QuickMedia { body_item->set_description_color(sf::Color(179, 179, 179)); body_item->url = "https://www.youtube.com/channel/" + channel_id_json.asString(); if(thumbnail) { - body_item->thumbnail_url = std::string("https:") + thumbnail->url; + if(string_starts_with(thumbnail->url, "https:")) + body_item->thumbnail_url = thumbnail->url; + else + body_item->thumbnail_url = std::string("https:") + thumbnail->url; body_item->thumbnail_mask_type = ThumbnailMaskType::CIRCLE; body_item->thumbnail_size.x = thumbnail->width; body_item->thumbnail_size.y = thumbnail->height; @@ -386,13 +415,16 @@ namespace QuickMedia { static std::vector get_cookies() { std::lock_guard lock(cookies_mutex); if(cookies_filepath.empty()) { + YoutubeSignatureDecryptor::get_instance(); + Path cookies_filepath_p; if(get_cookies_filepath(cookies_filepath_p, "youtube") != 0) { show_notification("QuickMedia", "Failed to create youtube cookies file", Urgency::CRITICAL); return {}; } - // TODO: Re-enable this if the api key ever changes in the future + // TODO: Re-enable this if the api key ever changes in the future. + // Maybe also put signature decryption in the same request? since it requests the same page. #if 0 //api_key = youtube_page_find_api_key(); #else @@ -655,6 +687,11 @@ namespace QuickMedia { return PluginResult::OK; } + PluginResult YoutubeSearchPage::lazy_fetch(BodyItems&) { + get_cookies(); + return PluginResult::OK; + } + PluginResult YoutubeSearchPage::search_get_continuation(const std::string &url, const std::string ¤t_continuation_token, BodyItems &result_items) { std::string next_url = url + "&pbj=1&ctoken=" + current_continuation_token; @@ -1800,11 +1837,237 @@ namespace QuickMedia { return std::make_unique(program, xsrf_token, comments_continuation_token); } - std::unique_ptr YoutubeVideoPage::create_related_videos_page(Program *program, const std::string&, const std::string&) { + std::unique_ptr YoutubeVideoPage::create_related_videos_page(Program *program) { return std::make_unique(program); } std::unique_ptr YoutubeVideoPage::create_channels_page(Program *program, const std::string &channel_url) { return std::make_unique(program, channel_url, "", "Channel videos"); } + + static std::map http_params_parse(const std::string &http_params) { + std::map result; + string_split(http_params, '&', [&result](const char *str, size_t size) { + const void *split_p = memchr(str, '=', size); + if(split_p == nullptr) + return true; + + std::string key(str, (const char*)split_p - str); + std::string value((const char*)split_p + 1, (str + size) - ((const char*)split_p + 1)); + key = url_param_decode(key); + value = url_param_decode(value); + result[std::move(key)] = std::move(value); + return true; + }); + return result; + } + + static std::string url_extract_param(const std::string &url, const std::string ¶m) { + std::string param_s = param + "="; + size_t index = url.find(param_s); + if(index == std::string::npos) + return ""; + + index += param_s.size(); + size_t end = url.find('&', index); + if(end == std::string::npos) + end = url.size(); + + return url.substr(index, end - index); + } + + std::string YoutubeVideoPage::get_video_url(int max_height, bool &has_embedded_audio) { + if(video_formats.empty()) { + has_embedded_audio = true; + return ""; + } + + for(const auto &video_format : video_formats) { + if(video_format.height <= max_height) { + has_embedded_audio = video_format.has_embedded_audio; + return video_format.base.url; + } + } + + has_embedded_audio = video_formats.back().has_embedded_audio; + return video_formats.back().base.url; + } + + std::string YoutubeVideoPage::get_audio_url() { + if(audio_formats.empty()) + return ""; + + return audio_formats.front().base.url; + } + + PluginResult YoutubeVideoPage::load() { + video_formats.clear(); + audio_formats.clear(); + + std::string video_id; + if(!youtube_url_extract_id(url, video_id)) { + fprintf(stderr, "Failed to extract youtube id from %s\n", url.c_str()); + return PluginResult::ERR; + } + + std::vector additional_args = { + { "-H", "x-youtube-client-name: 1" }, + { "-H", "x-youtube-client-version: 2.20200626.03.00" } + }; + + std::vector cookies = get_cookies(); + additional_args.insert(additional_args.end(), cookies.begin(), cookies.end()); + + std::string response; + DownloadResult download_result = download_to_string("https://www.youtube.com/get_video_info?html5=1&video_id=" + video_id + "&eurl=https://www.youtube.googleapis.com/v/" + video_id, response, std::move(additional_args), true); // TODO: true? + if(download_result != DownloadResult::OK) return download_result_to_plugin_result(download_result); + + std::string player_response_param = url_extract_param(response, "player_response"); + player_response_param = url_param_decode(player_response_param); + + Json::Value json_root; + Json::CharReaderBuilder json_builder; + std::unique_ptr json_reader(json_builder.newCharReader()); + std::string json_errors; + if(!json_reader->parse(player_response_param.data(), player_response_param.data() + player_response_param.size(), &json_root, &json_errors)) { + fprintf(stderr, "Failed to read param as json, error: %s\n", json_errors.c_str()); + return PluginResult::ERR; + } + + if(!json_root.isObject()) + return PluginResult::ERR; + + const Json::Value &streaming_data_json = json_root["streamingData"]; + if(!streaming_data_json.isObject()) + return PluginResult::ERR; + + parse_formats(streaming_data_json); + + if(video_formats.empty() && audio_formats.empty()) + return PluginResult::ERR; + + std::sort(video_formats.begin(), video_formats.end(), [](const YoutubeVideoFormat &format1, const YoutubeVideoFormat &format2) { + return format1.base.bitrate > format2.base.bitrate; + }); + + std::sort(audio_formats.begin(), audio_formats.end(), [](const YoutubeAudioFormat &format1, const YoutubeAudioFormat &format2) { + return format1.base.bitrate > format2.base.bitrate; + }); + + return PluginResult::OK; + } + + static bool parse_cipher_format(const Json::Value &format, YoutubeFormat &youtube_format) { + std::map cipher_params; + const Json::Value &cipher_json = format["cipher"]; + if(cipher_json.isString()) { + cipher_params = http_params_parse(cipher_json.asString()); + } else { + const Json::Value &signature_cipher_json = format["signatureCipher"]; + if(signature_cipher_json.isString()) + cipher_params = http_params_parse(signature_cipher_json.asString()); + } + + const std::string &url = cipher_params["url"]; + if(cipher_params.empty() || url.empty()) + return false; + + std::string url_decoded = url_param_decode(url); + + const std::string &s = cipher_params["s"]; + const std::string &sp = cipher_params["sp"]; + std::string sig_key; + std::string sig_value; + if(YoutubeSignatureDecryptor::get_instance().decrypt(s, sp, sig_key, sig_value)) + url_decoded += "&" + std::move(sig_key) + "=" + std::move(sig_value); + + youtube_format.url = std::move(url_decoded); + return true; + } + + void YoutubeVideoPage::parse_format(const Json::Value &format_json, bool is_adaptive) { + if(!format_json.isArray()) + return; + + for(const Json::Value &format : format_json) { + if(!format.isObject()) + continue; + + if(is_adaptive) { + // TODO: Fix. Some streams use sq/ instead of index + const Json::Value &index_range_json = format["indexRange"]; + if(index_range_json.isNull()) { + fprintf(stderr, "Ignoring adaptive stream without indexRange\n"); + continue; + } + } + + YoutubeFormat youtube_format_base; + + const Json::Value &mime_type_json = format["mimeType"]; + if(!mime_type_json.isString()) continue; + + const Json::Value &bitrate_json = format["bitrate"]; + if(!bitrate_json.isInt()) continue; + youtube_format_base.bitrate = bitrate_json.asInt(); + + if(strncmp(mime_type_json.asCString(), "video/", 6) == 0) { + bool has_embedded_audio = false; + const char *codecs_p = strstr(mime_type_json.asCString(), "codecs=\""); + if(codecs_p) { + codecs_p += 8; + const char *codecs_sep_p = strchr(codecs_p, ','); + const char *codecs_end_p = strchr(codecs_p, '"'); + has_embedded_audio = (codecs_sep_p != nullptr && (!codecs_end_p || codecs_sep_p < codecs_end_p)); + } + + YoutubeVideoFormat video_format; + video_format.base = std::move(youtube_format_base); + video_format.has_embedded_audio = has_embedded_audio; + + const Json::Value &width_json = format["width"]; + if(!width_json.isInt()) continue; + video_format.width = width_json.asInt(); + + const Json::Value &height_json = format["height"]; + if(!height_json.isInt()) continue; + video_format.height = height_json.asInt(); + + const Json::Value &fps_json = format["fps"]; + if(!fps_json.isInt()) continue; + video_format.fps = fps_json.asInt(); + + const Json::Value &url_json = format["url"]; + if(url_json.isString()) { + video_format.base.url = url_json.asString(); + } else { + if(!parse_cipher_format(format, video_format.base)) + continue; + } + + video_formats.push_back(std::move(video_format)); + } else if(strncmp(mime_type_json.asCString(), "audio/", 6) == 0) { + YoutubeAudioFormat audio_format; + audio_format.base = std::move(youtube_format_base); + + const Json::Value &url_json = format["url"]; + if(url_json.isString()) { + audio_format.base.url = url_json.asString(); + } else { + if(!parse_cipher_format(format, audio_format.base)) + continue; + } + + audio_formats.push_back(std::move(audio_format)); + } + } + } + + void YoutubeVideoPage::parse_formats(const Json::Value &streaming_data_json) { + const Json::Value &formats_json = streaming_data_json["formats"]; + parse_format(formats_json, false); + + const Json::Value &adaptive_formats_json = streaming_data_json["adaptiveFormats"]; + parse_format(adaptive_formats_json, false); + } } \ No newline at end of file diff --git a/src/plugins/youtube/Signature.cpp b/src/plugins/youtube/Signature.cpp new file mode 100644 index 0000000..30093d5 --- /dev/null +++ b/src/plugins/youtube/Signature.cpp @@ -0,0 +1,307 @@ +#include "../../../plugins/youtube/Signature.hpp" +#include "../../../include/Storage.hpp" +#include "../../../include/Notification.hpp" +#include "../../../include/DownloadUtils.hpp" +#include "../../../include/StringUtils.hpp" +#include "../../../include/Program.hpp" +#include +#include + +namespace QuickMedia { + enum UpdateDecryptFunctionError { + U_DEC_FUN_NET_ERR = 1, + U_DEC_FUN_FAILED_MATCH_ERR = 2 + }; + + static YoutubeSignatureDecryptor *instance = nullptr; + static const int timeout_default_sec = 60; + + bool YoutubeSignatureDecryptor::js_code_to_operations(const std::string &function_body_str, const std::string &var_body_str, std::vector &new_func_calls, std::map &new_func_decls) { + std::vector function_body; + string_split(function_body_str, ';', [&function_body](const char *str, size_t size) { + function_body.push_back(std::string(str, size)); + return true; + }); + + if(function_body.empty()) + return false; + + std::vector var_body; + string_split(var_body_str, "},", [&var_body](const char *str, size_t size) { + var_body.push_back(std::string(str, size)); + return true; + }); + + if(var_body.empty()) + return false; + + //fprintf(stderr, "function body: %s\n", function_body_str.c_str()); + for(const std::string &func_call_str : function_body) { + const size_t var_name_split_index = func_call_str.find('.'); + if(var_name_split_index == std::string::npos) return false; + + const size_t func_name_end_index = func_call_str.find('(', var_name_split_index + 1); + if(func_name_end_index == std::string::npos) return false; + + const size_t values_index = func_call_str.find(',', func_name_end_index + 1); + if(values_index == std::string::npos) return false; + + const size_t values_end_index = func_call_str.find(')', values_index + 1); + if(values_end_index == std::string::npos) return false; + + std::string func_name = func_call_str.substr(var_name_split_index + 1, func_name_end_index - (var_name_split_index + 1)); + func_name = strip(func_name); + std::string value_args = func_call_str.substr(values_index + 1, values_end_index - (values_index + 1)); + + errno = 0; + char *endptr; + const long value_int = strtol(value_args.c_str(), &endptr, 10); + if(endptr != value_args.c_str() && errno == 0) + new_func_calls.push_back({ std::move(func_name), value_int }); + else + return false; + + //fprintf(stderr, "func_call: %s, value: %ld\n", new_func_calls.back().func_name.c_str(), value_int); + } + + //fprintf(stderr, "declaration body: %s\n", var_body_str.c_str()); + for(const std::string &func_decl_str : var_body) { + const size_t func_name_split_index = func_decl_str.find(':'); + if(func_name_split_index == std::string::npos) return false; + + const size_t func_start_index = func_decl_str.find('{', func_name_split_index + 1); + if(func_start_index == std::string::npos) return false; + + std::string func_name = func_decl_str.substr(0, func_name_split_index); + func_name = strip(func_name); + std::string function_body = func_decl_str.substr(func_start_index + 1); + function_body = strip(function_body); + if(!function_body.empty() && function_body.back() == '}') + function_body.pop_back(); + + DecryptFunction decrypt_function; + if(function_body == "a.reverse()") + decrypt_function = DecryptFunction::REVERSE; + else if(function_body == "a.splice(0,b)") + decrypt_function = DecryptFunction::SPLICE; + else + decrypt_function = DecryptFunction::SWAP; + //fprintf(stderr, "declared function: %s, body: |%s|\n", func_name.c_str(), function_body.c_str()); + new_func_decls[std::move(func_name)] = decrypt_function; + } + + for(const auto &func_call : new_func_calls) { + if(new_func_decls.find(func_call.func_name) == new_func_decls.end()) { + fprintf(stderr, "YoutubeSignatureDecryptor: declaration for decryption function %s not found\n", func_call.func_name.c_str()); + fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 10 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n"); + return false; + } + } + + return true; + } + + YoutubeSignatureDecryptor::YoutubeSignatureDecryptor() { + { + Path youtube_cache_dir = get_cache_dir().join("youtube"); + if(create_directory_recursive(youtube_cache_dir) != 0) { + show_notification("QuickMedia", "Failed to create youtube cache directory", Urgency::CRITICAL); + return; + } + + youtube_cache_dir.join("decryption_function"); + if(file_get_content(youtube_cache_dir, decryption_function) == 0) { + file_get_last_modified_time_seconds(youtube_cache_dir.data.c_str(), &decrypt_function_last_updated); + + size_t newline_index = decryption_function.find('\n'); + if(newline_index != std::string::npos) { + std::string function_body_str = decryption_function.substr(0, newline_index); + std::string var_body_str = decryption_function.substr(newline_index + 1); + + std::vector new_func_calls; + std::map new_func_decls; + if(js_code_to_operations(function_body_str, var_body_str, new_func_calls, new_func_decls)) { + func_calls = std::move(new_func_calls); + func_decls = std::move(new_func_decls); + + time_t time_now = time(nullptr); + if(time_now - decrypt_function_last_updated <= timeout_default_sec) + up_to_date = true; + } + } + } + } + + running = true; + poll_task = AsyncTask([this]() mutable { + bool has_notified_error = false; + int decrypt_function_update_timeout_seconds = timeout_default_sec; + while(running) { + time_t time_now = time(nullptr); + if(time_now - decrypt_function_last_updated > decrypt_function_update_timeout_seconds) { + int update_res = update_decrypt_function(); + if(update_res != 0) { + if(!has_notified_error) { + if(program_is_dead_in_current_thread()) { + running = false; + return; + } else if(update_res == U_DEC_FUN_NET_ERR) { + show_notification("QuickMedia", "Failed to decrypt youtube signature. Is your internet down?", Urgency::CRITICAL); + decrypt_function_update_timeout_seconds = 10; + } else if(update_res == U_DEC_FUN_FAILED_MATCH_ERR) { + show_notification("QuickMedia", "Failed to decrypt youtube signature. Make sure you are running the latest version of QuickMedia", Urgency::CRITICAL); + running = false; + return; + } + } + has_notified_error = true; + } else { + decrypt_function_update_timeout_seconds = timeout_default_sec; + } + } + usleep(1 * 1000 * 1000); // 1 second + } + }); + } + + YoutubeSignatureDecryptor::~YoutubeSignatureDecryptor() { + running = false; + } + + // static + YoutubeSignatureDecryptor& YoutubeSignatureDecryptor::get_instance() { + if(!instance) + instance = new YoutubeSignatureDecryptor(); + return *instance; + } + + bool YoutubeSignatureDecryptor::decrypt(const std::string &s, const std::string &sp, std::string &sig_key, std::string &sig_value) { + if(s.empty() || sp.empty()) + return false; + + if(!up_to_date) { + int num_tries = 0; + const int max_tries = 30; + while(running && num_tries < max_tries && !program_is_dead_in_current_thread()) { // 6 seconds in total + std::lock_guard lock(update_signature_mutex); + if(up_to_date) + break; + ++num_tries; + usleep(200 * 1000); // 200 milliseconds + } + + if(num_tries == max_tries) { + show_notification("QuickMedia", "Failed to get decryption function for youtube. Make sure your internet is up and that you are running the latest version of QuickMedia", Urgency::CRITICAL); + return false; + } + } + + std::lock_guard lock(update_signature_mutex); + std::string sig = s; + for(const auto &func_call : func_calls) { + auto func_decl_it = func_decls.find(func_call.func_name); + assert(func_decl_it != func_decls.end()); + + switch(func_decl_it->second) { + case DecryptFunction::REVERSE: { + std::reverse(sig.begin(), sig.end()); + break; + } + case DecryptFunction::SPLICE: { + long int erase_index = func_call.arg; + if(erase_index > 0 && (size_t)erase_index < sig.size()) + sig.erase(0, erase_index); + break; + } + case DecryptFunction::SWAP: { + if(sig.empty()) { + fprintf(stderr, "YoutubeSignatureDecryptor: sig unexpectedly empty in swap\n"); + } else { + char c = sig[0]; + sig[0] = sig[func_call.arg % sig.size()]; + sig[func_call.arg % sig.size()] = c; + } + break; + } + } + } + + sig_key = sp; + sig_value = sig; + return true; + } + + int YoutubeSignatureDecryptor::update_decrypt_function() { + std::string response; + DownloadResult download_result = download_to_string("https://www.youtube.com/watch?v=CvFH_6DNRCY&gl=US&hl=en", response, {}, true); + if(download_result != DownloadResult::OK) { + fprintf(stderr, "YoutubeSignatureDecryptor::update_decrypt_function failed. Failed to get youtube page\n"); + return U_DEC_FUN_NET_ERR; + } + + std::smatch base_js_match; + if(!std::regex_search(response, base_js_match, std::regex(R"END((\/s\/player\/[^\/]+\/player_ias[^\/]+\/en_US\/base\.js))END", std::regex::ECMAScript)) || base_js_match.size() != 2) { + fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 1 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n"); + return U_DEC_FUN_FAILED_MATCH_ERR; + } + + const std::string &url = base_js_match[1].str(); + download_result = download_to_string("https://www.youtube.com" + url, response, {}, true); + if(download_result != DownloadResult::OK) { + fprintf(stderr, "YoutubeSignatureDecryptor::update_decrypt_function failed. Failed to get https://www.youtube.com%s\n", url.c_str()); + return U_DEC_FUN_NET_ERR; + } + + std::smatch function_match; + if(!std::regex_search(response, function_match, std::regex(R"END((^|\n)\w+=function\(\w\)\{\w=\w\.split\(""\);([^\}]*)\})END", std::regex::ECMAScript)) || function_match.size() != 3) { + fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 2 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n"); + return U_DEC_FUN_FAILED_MATCH_ERR; + } + + std::string function_body_str = function_match[2].str(); + size_t last_semicolon_index = function_body_str.rfind(';'); + if(last_semicolon_index == std::string::npos) { + fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 3 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n"); + return U_DEC_FUN_FAILED_MATCH_ERR; + } + + function_body_str.erase(last_semicolon_index, function_body_str.size()); + string_replace_all(function_body_str, '\n', ' '); + + size_t var_dot_index = function_body_str.find('.'); + if(var_dot_index == std::string::npos) { + fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 5 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n"); + return U_DEC_FUN_FAILED_MATCH_ERR; + } + + std::string var_name = function_body_str.substr(0, var_dot_index); + string_replace_all(response, '\n', ' '); + std::smatch var_body_match; + if(!std::regex_search(response, var_body_match, std::regex("var " + var_name + "=\\{(.*?)\\};", std::regex::ECMAScript)) || var_body_match.size() != 2) { + fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 6 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n"); + return U_DEC_FUN_FAILED_MATCH_ERR; + } + + std::string var_body_str = var_body_match[1].str(); + string_replace_all(var_body_str, '\n', ' '); + + std::vector new_func_calls; + std::map new_func_decls; + if(!js_code_to_operations(function_body_str, var_body_str, new_func_calls, new_func_decls)) { + fprintf(stderr, "YoutubeSignatureDecryptor: Regex match 7 invalid. Youtube likely updated and QuickMedia needs to be fixed?\n"); + return U_DEC_FUN_FAILED_MATCH_ERR; + } + + { + std::lock_guard lock(update_signature_mutex); + decryption_function = function_body_str + "\n" + var_body_str; + decrypt_function_last_updated = time(nullptr); + up_to_date = true; + func_calls = std::move(new_func_calls); + func_decls = std::move(new_func_decls); + } + + file_overwrite_atomic(get_cache_dir().join("youtube").join("decryption_function"), decryption_function); + return 0; + } +} \ No newline at end of file -- cgit v1.2.3