#include "../../plugins/DramaCool.hpp" #include "../../include/Theme.hpp" #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 #include #include // 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 { SearchResult DramaCoolSearchPage::search(const std::string &str, BodyItems &result_items) { if(str.empty()) return SearchResult::OK; std::vector additional_args = { { "-H", "x-requested-with: XMLHttpRequest" } }; Json::Value json_root; DownloadResult result = download_json(json_root, "https://dramacool.cr/search?keyword=" + url_param_encode(str) + "&type=movies", std::move(additional_args), true); if(result != DownloadResult::OK) return download_result_to_search_result(result); if(!json_root.isArray()) return SearchResult::ERR; for(const Json::Value &json_item : json_root) { if(!json_item.isObject()) continue; const Json::Value &name_json = json_item["name"]; const Json::Value &status_json = json_item["status"]; const Json::Value &cover_url_json = json_item["cover"]; const Json::Value &url_json = json_item["url"]; if(!name_json.isString() || !url_json.isString()) continue; auto body_item = BodyItem::create(name_json.asString()); if(status_json.isString()) { body_item->set_description(status_json.asString()); body_item->set_description_color(get_theme().faded_text_color); } if(cover_url_json.isString()) { body_item->thumbnail_url = "https://asianimg.pro/" + url_param_encode(cover_url_json.asString()); body_item->thumbnail_size = { 150, 225 }; } body_item->url = "https://dramacool.cr" + url_json.asString(); result_items.push_back(std::move(body_item)); } return SearchResult::OK; } static bool node_get_inner_text(const QuickMediaHtmlChildNode *node, QuickMediaStringView &str) { if(!node || !node->node.is_tag || !node->node.first_child || node->node.first_child->node.is_tag) return false; str = node->node.first_child->node.name; return true; } PluginResult DramaCoolSearchPage::submit(const SubmitArgs &args, std::vector &result_tabs) { std::string website_data; DownloadResult result = download_to_string(args.url, website_data, {}, true); if(result != DownloadResult::OK) return download_result_to_plugin_result(result); BodyItems result_items; QuickMediaHtmlSearch html_search; int res = quickmedia_html_search_init(&html_search, website_data.c_str(), website_data.size()); if(res != 0) return PluginResult::ERR; quickmedia_html_find_nodes_xpath(&html_search, "//div[class='content']//a", [](QuickMediaMatchNode *node, void *userdata) { auto *result_items = (BodyItems*)userdata; QuickMediaStringView href = quickmedia_html_node_get_attribute_value(node->node, "href"); if(!href.data || !memmem(href.data, href.size, ".html", 5)) return 0; QuickMediaHtmlChildNode *sub_node = node->node->first_child; if(!sub_node) return 0; QuickMediaHtmlChildNode *title_node = sub_node->next; if(!title_node) return 0; QuickMediaHtmlChildNode *time_node = title_node->next; if(!time_node) return 0; QuickMediaStringView sub_str; QuickMediaStringView title_str; QuickMediaStringView time_str; if(!node_get_inner_text(sub_node, sub_str) || !node_get_inner_text(title_node, title_str) || !node_get_inner_text(time_node, time_str)) return 0; std::string title(title_str.data, title_str.size); html_unescape_sequences(title); std::string time(time_str.data, time_str.size); html_unescape_sequences(time); const bool is_subbed = sub_str.size == 3 && memcmp(sub_str.data, "SUB", 3) == 0; auto body_item = BodyItem::create(std::move(title)); body_item->set_description((is_subbed ? "Subbed" : "Raw") + std::string(" • ") + time); body_item->set_description_color(get_theme().faded_text_color); body_item->url = "https://dramacool.cr" + std::string(href.data, href.size); result_items->push_back(std::move(body_item)); return 0; }, &result_items); quickmedia_html_search_deinit(&html_search); auto body = create_body(); body->set_items(std::move(result_items)); result_tabs.push_back(Tab{ std::move(body), std::make_unique(program, args.title), create_search_bar("Search...", SEARCH_DELAY_FILTER) }); return PluginResult::OK; } struct VideoSources { //std::string streamsss; std::string asianload; std::string streamtape; std::string mixdrop; std::string mp4upload; std::string doodstream; }; static bool dembed_extract_video_source(const std::string &website_data, const std::string &video_source_url, std::string &video_source) { size_t st_start = website_data.find(video_source_url); if(st_start == std::string::npos) return false; st_start += video_source_url.size(); size_t st_end = website_data.find("\"", st_start); if(st_end == std::string::npos) return false; video_source = "https://" + video_source_url + website_data.substr(st_start, st_end - st_start); return true; } 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); dembed_extract_video_source(website_data, "dood.wf", video_sources.doodstream); } // TODO: Re-add. It's broken right now (because of the json_url has incorrect value I guess) /* static bool streamsss_extract_video_url(Page *page, const std::string &streamsss_url, std::string &url) { size_t url_end = streamsss_url.find("/e"); if(url_end == std::string::npos) return false; size_t id_start = streamsss_url.find("e/"); if(id_start == std::string::npos) return false; id_start += 2; size_t id_end = streamsss_url.find("?", id_start); if(id_end == std::string::npos) id_end = streamsss_url.size(); const std::string url_base = streamsss_url.substr(0, url_end); const std::string id = streamsss_url.substr(id_start, id_end - id_start); std::ostringstream id_hex; id_hex << std::hex; for(size_t i = 0; i < id.size(); ++i) id_hex << (int)(unsigned char)id[i]; const std::string json_url = url_base + "/sources43/566d337678566f743674494a7c7c" + id_hex.str() + "7c7c346b6767586d6934774855537c7c73747265616d7362/6565417268755339773461447c7c346133383438333436313335376136323337373433383634376337633465366534393338373136643732373736343735373237613763376334363733353737303533366236333463353333363534366137633763373337343732363536313664373336327c7c6b586c3163614468645a47617c7c73747265616d7362"; Json::Value json_root; DownloadResult result = page->download_json(json_root, json_url, {}, true); if(result != DownloadResult::OK) return false; if(!json_root.isObject()) return false; const Json::Value &stream_data_json = json_root["stream_data"]; if(!stream_data_json.isObject()) return false; const Json::Value &file_json = stream_data_json["file"]; if(!file_json.isString()) return false; // TODO: Record resolution and duration std::string website_data; result = download_to_string(file_json.asString(), website_data, {}, true); 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; } */ static bool streamtape_extract_video_url(const std::string &website_data, std::string &url) { size_t id_start = website_data.find("getElementById('robotlink')"); if(id_start == std::string::npos) return false; id_start += 27; id_start = website_data.find("id=", id_start); if(id_start == std::string::npos) return false; id_start += 3; size_t id_end = website_data.find("'", id_start); if(id_end == std::string::npos) return false; url = "https://streamtape.com/get_video?id=" + website_data.substr(id_start, id_end - id_start); return true; } static std::vector extract_javascript_sections(const std::string &html_source) { std::vector sections; size_t start = 0; while(true) { start = html_source.find("", start); if(start == std::string::npos) break; start += 1; size_t end = html_source.find("", start); if(end == std::string::npos) break; sections.push_back(html_source.substr(start, end - start)); start = end + 9; } return sections; } static std::vector mixdrop_extract_mdcore_scripts(const std::string &website_data) { std::vector scripts; for(const std::string &js_section : extract_javascript_sections(website_data)) { size_t eval_index = js_section.find("eval"); if(eval_index == std::string::npos) continue; eval_index += 4; size_t start = eval_index; while(true) { size_t script_start = js_section.find("\"//", start); if(script_start == std::string::npos) { script_start = js_section.find("://", start); if(script_start == std::string::npos) break; } script_start += 3; size_t script_end = js_section.find("\"", script_start); if(script_end == std::string::npos) break; size_t url_start = script_start - 2; scripts.push_back(js_section.substr(url_start, script_end - url_start)); start = script_end + 1; } } return scripts; } static bool mixdrop_extract_mdcore_parts(const std::string &website_data, std::vector &parts) { size_t mdcore_start = website_data.find(",'|"); if(mdcore_start == std::string::npos) { mdcore_start = website_data.find("MDCore|"); if(mdcore_start == std::string::npos) return false; } else { mdcore_start += 2; } size_t mdcore_end = website_data.find("'", mdcore_start); if(mdcore_end == std::string::npos) return false; std::string mdcore_str = website_data.substr(mdcore_start, mdcore_end - mdcore_start); string_split(mdcore_str, '|', [&parts](const char *str, size_t size) { parts.emplace_back(str, size); return true; }); return true; } static int mdcore_number_get(char c) { if(c >= '0' && c <= '9') return c - '0'; else if(c >= 'a' && c <= 'z') return 10 + (c - 'a'); else return -1; } static std::string mdcore_script_decode(const std::string &mdcore_script, const std::vector &mdcore_parts) { std::string decoded; for(size_t i = 0; i < mdcore_script.size();) { char c = mdcore_script[i]; int index = mdcore_number_get(c); if(index >= 0) { ++i; while(i < mdcore_script.size() && ((mdcore_script[i] >= '0' && mdcore_script[i] <= '9') || (mdcore_script[i] >= 'a' && mdcore_script[i] <= 'z'))) { // 36 = 0-9 + a-z index = (index * 36) + mdcore_number_get(mdcore_script[i]); ++i; } } else { decoded += c; ++i; continue; } if(index >= (int)mdcore_parts.size() || mdcore_parts[index].empty()) decoded += c; else decoded += mdcore_parts[index]; } if(string_starts_with(decoded, "//")) decoded = "https:" + decoded; return decoded; } static bool mixdrop_extract_video_url(const std::string &website_data, std::string &url) { std::vector mdcore_parts; if(!mixdrop_extract_mdcore_parts(website_data, mdcore_parts)) return false; for(const std::string &mdcore_script : mixdrop_extract_mdcore_scripts(website_data)) { std::string decoded_url = mdcore_script_decode(mdcore_script, mdcore_parts); if(decoded_url.find("/v/") != std::string::npos || decoded_url.find("/d/") != std::string::npos) { url = std::move(decoded_url); return true; } } return false; } static bool generate_random_string_doodstream(char *buffer, int buffer_size) { return generate_random_characters(buffer, buffer_size, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789", 62); } static bool doodstream_extract_video_url(const std::string &website_data, std::string &url, const std::string &referer) { const size_t pass_start = website_data.find("/pass_md5/"); if(pass_start == std::string::npos) return false; const size_t pass_end1 = website_data.find("'", pass_start + 10); const size_t pass_end2 = website_data.find("\"", pass_start + 10); size_t pass_end = pass_end1; if(pass_end2 != std::string::npos && pass_end2 < pass_end) pass_end = pass_end2; if(pass_end == std::string::npos) return false; const std::string pass_url = "https://dood.wf" + website_data.substr(pass_start, pass_end - pass_start); std::string video_url; DownloadResult result = download_to_string(pass_url, video_url, {{ "-H", "Referer: " + referer }}, true); if(result != DownloadResult::OK) return false; const size_t token_start = pass_url.rfind('/'); if(token_start == std::string::npos) return false; const std::string token = pass_url.substr(token_start + 1); char random_str[10]; if(!generate_random_string_doodstream(random_str, sizeof(random_str))) return false; video_url.append(random_str, sizeof(random_str)); video_url += "?token=" + token + "&expiry=" + std::to_string((int64_t)time(NULL) * 1000LL); url = std::move(video_url); return true; } static std::string url_extract_param(const std::string &url, const std::string ¶m_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(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(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_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 &result_tabs) { std::string website_data; DownloadResult result = download_to_string(args.url, website_data, {}, true); if(result != DownloadResult::OK) return download_result_to_plugin_result(result); std::string video_sources_url; QuickMediaHtmlSearch html_search; int res = quickmedia_html_search_init(&html_search, website_data.c_str(), website_data.size()); if(res != 0) return PluginResult::ERR; quickmedia_html_find_nodes_xpath(&html_search, "//div", [](QuickMediaMatchNode *node, void *userdata) { auto *video_sources_url = (std::string*)userdata; QuickMediaStringView klass = quickmedia_html_node_get_attribute_value(node->node, "class"); if(!klass.data || !memmem(klass.data, klass.size, "watch_video", 11)) return 0; QuickMediaHtmlChildNode *iframe_node = node->node->first_child; if(!iframe_node || !iframe_node->node.is_tag || iframe_node->node.name.size != 6 || memcmp(iframe_node->node.name.data, "iframe", 6) != 0) return 0; QuickMediaStringView src = quickmedia_html_node_get_attribute_value(&iframe_node->node, "src"); if(!src.data) return 0; video_sources_url->assign(src.data, src.size); return 1; }, &video_sources_url); quickmedia_html_search_deinit(&html_search); if(video_sources_url.empty()) return PluginResult::ERR; if(string_starts_with(video_sources_url, "//")) video_sources_url = "https:" + video_sources_url; result = download_to_string(video_sources_url, website_data, {}, true); if(result != DownloadResult::OK) return download_result_to_plugin_result(result); // TODO: Extract all video sources and allow the user to select which one to use. // Streamtape or mixdrop may not be available or if it's available it may not load properly // or the quality may be worse or slower than other sources. // We also want to load the high resolution version of the video. // TODO: Make videos sources work even when captions are not embedded. VideoSources video_sources; dembed_extract_video_sources(website_data, video_sources); 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) { streamtape_extract_video_url(website_data, video_url); if(!video_url.empty()) referer = "https://streamtape.com"; } } if(!video_sources.mixdrop.empty() && video_url.empty()) { result = download_to_string(video_sources.mixdrop, website_data, {}, true); if(result == DownloadResult::OK) { mixdrop_extract_video_url(website_data, video_url); if(!video_url.empty()) referer = "https://mixdrop.co"; } } if(!video_sources.mp4upload.empty() && video_url.empty()) { result = download_to_string(video_sources.mp4upload, website_data, {}, true); if(result == DownloadResult::OK) { // mp4upload uses the same algorithm as mixdrop but with a different format. // |mixdrop_extract_video_url| handles both formats. mixdrop_extract_video_url(website_data, video_url); if(!video_url.empty()) referer = "https://www.mp4upload.com"; } } if(!video_sources.doodstream.empty() && video_url.empty()) { result = download_to_string(video_sources.doodstream, website_data, {}, true); if(result == DownloadResult::OK) { doodstream_extract_video_url(website_data, video_url, video_sources.doodstream); if(!video_url.empty()) referer = "https://dood.wf"; } } if(video_url.empty()) return PluginResult::ERR; /* if(!video_sources.streamsss.empty() && video_url.empty()) { streamsss_extract_video_url(this, video_sources.streamsss, video_url); } */ result_tabs.push_back(Tab{ nullptr, std::make_unique(program, std::move(video_url), series_name, args.title, std::move(referer)), nullptr }); return PluginResult::OK; } PluginResult DramaCoolVideoPage::load(const SubmitArgs&, VideoInfo &video_info, std::string &err_str) { video_info.title = episode_name; video_info.channel_url.clear(); video_info.duration = 0.0; video_info.chapters.clear(); video_info.referer = referer; err_str.clear(); return PluginResult::OK; } std::string DramaCoolVideoPage::get_url_timestamp() { // TODO: Remove very old videos, to not make this file too large which slows this down on slow harddrives std::unordered_map watch_progress = get_watch_progress_for_plugin("dramacool"); auto it = watch_progress.find(get_video_id()); if(it == watch_progress.end()) return ""; // If we are very close to the end then start from the beginning. // This is the same behavior as mpv. // This is better because we dont want the video player to stop immediately after we start playing and we dont get any chance to seek. if(it->second.time_pos_sec + 10.0 >= it->second.duration_sec) return ""; else return std::to_string(it->second.time_pos_sec); } void DramaCoolVideoPage::set_watch_progress(int64_t time_pos_sec, int64_t duration_sec) { std::string video_id = get_video_id(); set_watch_progress_for_plugin("dramacool", video_id, time_pos_sec, duration_sec, video_id); } std::string DramaCoolVideoPage::get_video_id() const { return series_name + "/" + episode_name; } }