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/plugins/youtube/Signature.cpp | 307 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 307 insertions(+) create mode 100644 src/plugins/youtube/Signature.cpp (limited to 'src/plugins/youtube/Signature.cpp') 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