From 87c8a2986d468a3fc897169c1b00fc4695e09d39 Mon Sep 17 00:00:00 2001 From: dec05eba Date: Sun, 4 Sep 2022 05:01:36 +0200 Subject: Add dramacool --- src/plugins/DramaCool.cpp | 412 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 412 insertions(+) create mode 100644 src/plugins/DramaCool.cpp (limited to 'src/plugins/DramaCool.cpp') diff --git a/src/plugins/DramaCool.cpp b/src/plugins/DramaCool.cpp new file mode 100644 index 0000000..9b419f9 --- /dev/null +++ b/src/plugins/DramaCool.cpp @@ -0,0 +1,412 @@ +#include "../../plugins/DramaCool.hpp" +#include "../../include/Theme.hpp" +#include "../../include/StringUtils.hpp" +#include "../../include/M3U8.hpp" +#include +#include + +// 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://imagecdn.me/" + 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), create_search_bar("Search...", SEARCH_DELAY_FILTER) }); + return PluginResult::OK; + } + + struct VideoSources { + //std::string streamsss; + std::string streamtape; + std::string mixdrop; + std::string mp4upload; + }; + + 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, "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); + } + + // 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; + + 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 bool mixdrop_extract_mdcore_script(const std::string &website_data, const std::string &prefix, std::string &mdcore_script) { + size_t script_start = website_data.find(prefix); + if(script_start == std::string::npos) + return false; + + script_start += prefix.size(); + size_t script_end = website_data.find("\"", script_start); + if(script_end == std::string::npos) + return false; + + mdcore_script = website_data.substr(script_start, script_end - script_start); + return true; + } + + 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 bool mixdrop_extract_video_url(const std::string &website_data, const std::string &prefix, std::string &url) { + std::string mdcore_script; + if(!mixdrop_extract_mdcore_script(website_data, prefix, mdcore_script)) + return false; + + std::vector mdcore_parts; + if(!mixdrop_extract_mdcore_parts(website_data, mdcore_parts)) + return false; + + 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 { + url += c; + ++i; + continue; + } + + if(index >= (int)mdcore_parts.size() || mdcore_parts[index].empty()) + url += c; + else + url += mdcore_parts[index]; + } + + if(string_starts_with(url, "//")) + url = "http:" + url; + + return true; + } + + 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.streamtape.empty() && video_url.empty()) { + result = download_to_string(video_sources.streamtape, website_data, {}, true); + if(result != DownloadResult::OK) return download_result_to_plugin_result(result); + 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) return download_result_to_plugin_result(result); + mixdrop_extract_video_url(website_data, "0.f=\"", 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) return download_result_to_plugin_result(result); + // mp4upload uses the same algorithm as mixdrop but with different format + mixdrop_extract_video_url(website_data, "2.25(\"", video_url); + + if(!video_url.empty()) + referer = "https://www.mp4upload.com"; + } + + 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), args.title, std::move(referer)), nullptr }); + + return PluginResult::OK; + } + + PluginResult DramaCoolVideoPage::load(const SubmitArgs&, VideoInfo &video_info, std::string &err_str) { + video_info.title = title; + video_info.channel_url.clear(); + video_info.duration = 0.0; + video_info.chapters.clear(); + video_info.referer = referer; + err_str.clear(); + return PluginResult::OK; + } +} \ No newline at end of file -- cgit v1.2.3