diff --git a/CMakeLists.txt b/CMakeLists.txt index 3146e13..f6bd781 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ project(dropout-dl) set(CMAKE_CXX_STANDARD 17) -add_executable(dropout-dl main.cpp) +add_executable(dropout-dl src/episode.cpp src/main.cpp) IF (EXISTS "firefox_profile") target_link_libraries(dropout-dl curl sqlite3) diff --git a/src/episode.cpp b/src/episode.cpp new file mode 100644 index 0000000..72154ba --- /dev/null +++ b/src/episode.cpp @@ -0,0 +1,468 @@ +// +// Created by moss on 9/28/22. +// + +#include "episode.h" + +namespace dropout_dl { + + // dropout-dl helpers + bool substr_is(const std::string& string, int start, const std::string& test_str) { + if (test_str.size() != test_str.size()) + return false; + + for (int i = start, j = 0; i < start + test_str.size(); i++, j++) { + if (string[i] != test_str[j]) { + return false; + } + } + return true; + } + + void replace_all(std::string& str, const std::string& from, const std::string& to) { + size_t start_pos = 0; + while((start_pos = str.find(from, start_pos)) != std::string::npos) { + str.replace(start_pos, from.length(), to); + start_pos += to.length(); // Handles case where 'to' is a substring of 'from' + } + } + + #if defined(__WIN32__) + #include + msec_t time_ms(void) + { + return timeGetTime(); + } + #else + #include + long time_ms() + { + timeval tv{}; + gettimeofday(&tv, nullptr); + return tv.tv_sec * 1000 + tv.tv_usec / 1000; + } + #endif + + + long current_time; + long last_progress_timestamp; + + int curl_progress_func(void* ptr, double total_to_download, double downloaded, double total_to_upload, double uploaded) { + const double number_chars = 100; + const char* full_character = "▓"; + const char* empty_character = "░"; + + current_time = time_ms(); + if (current_time - 50 > last_progress_timestamp) { + double percent_done = (downloaded / total_to_download) * number_chars; + double percent_done_clone = percent_done; + putchar('['); + while (percent_done_clone-- > 0) { + std::cout << full_character; + } + while (percent_done++ < number_chars) { + std::cout << empty_character; + } + putchar(']'); + putchar(' '); + std::cout << downloaded / 1048576 << "MiB / " << total_to_download / 1048576 << "MiB "; + putchar('\r'); + last_progress_timestamp = time_ms(); + std::cout.flush(); + } + return 0; + } + + // episode statics + std::string episode::get_series_name(const std::string& html_data) { + std::string series_title("series-title"); + std::string open_a_tag(""); + std::string close_a(""); + + int series_name_start = -1; + + for (int i = 0; i < html_data.size(); i++) { + if (substr_is(html_data, i, series_title)) { + for (int j = i + series_title.size(); j < html_data.size(); j++) { + if (html_data[j] == '\n' || html_data[j] == ' ' || html_data[j] == '\t') continue; + if (substr_is(html_data, j, open_a_tag)) { + for (int k = j + open_a_tag.size(); k < html_data.size(); k++) { + if (substr_is(html_data, k, close_tag)) { + for (int l = 0; l < html_data.size() - k; l++) { + char c = html_data[k + l]; + if (series_name_start == -1) { + if (html_data[k + l + 1] == '\n' || html_data[k + l + 1] == ' ' || + html_data[k + l + 1] == '\t') { + continue; + } else { + series_name_start = k + l + 1; + } + } + if (substr_is(html_data, k + l, close_a) || (series_name_start != -1 && html_data[k + l] == '\n')) { + return html_data.substr(series_name_start, l - (series_name_start - k)); + } + } + } + } + } + } + } + } + return "-1"; + } + + std::string episode::get_episode_name(const std::string& html_data) { + int title_start = -1; + std::string video_title("video-title"); + std::string open_strong(""); + std::string close_strong(""); + for (int i = 0; i < html_data.size(); i++) { + if (substr_is(html_data, i, video_title)) { + for (int j = i; j < html_data.size(); j++) { + if (substr_is(html_data, j, open_strong)) { + title_start = j + 8; + break; + } + } + for (int j = 0; j < html_data.size() - title_start; j++) { + if (substr_is(html_data, title_start + j, close_strong)) { + return html_data.substr(title_start, j); + } + } + } + } + return "ERROR"; + } + + std::string episode::get_episode_number(const std::string& html_data) { + std::string episode("Episode"); + std::string close_a(""); + std::string episode_num; + for (int i = 0; i < html_data.size(); i++) { + if (substr_is(html_data, i, episode)) { + for (int j = i + 8; j < html_data.size(); j++) { + if (html_data[j] == '\n' || html_data[j] == ' ' || html_data[j] == '\t') continue; + if (substr_is(html_data, j, close_a)) { + return episode_num; + } + episode_num += html_data[j]; + } + } + } + return "-1"; + } + + std::string episode::get_season_number(const std::string& html_data) { + std::string season("Season"); + std::string dash(","); + std::string season_num; + for (int i = 0; i < html_data.size(); i++) { + if (substr_is(html_data, i, season)) { + for (int j = i + 7; j < html_data.size(); j++) { + if (html_data[j] == '\n' || html_data[j] == ' ' || html_data[j] == '\t') continue; + if (html_data[j] == '-' || html_data[j] == ',' ) { + return season_num; + } + season_num += html_data[j]; + } + } + } + return "-1"; + } + + std::string episode::get_embed_url(const std::string& html_data) { + std::string config("window.VHX.config"); + std::string embed_url("embed_url: "); + for (int i = 0; i < html_data.size(); i++) { + if (substr_is(html_data, i, config)) { + for (int j = i + config.size(); j < html_data.size(); j++) { + if (substr_is(html_data, j, embed_url)) { + for (int k = 0; k < html_data.size(); k++) { + if (html_data[k + j + embed_url.size() + 1] == '"') { + return html_data.substr(j + embed_url.size() + 1, k); + } + } + } + } + } + } + return ""; + } + + std::string episode::get_config_url(const std::string& html_data) { + std::string OTTdata("OTTData"); + std::string config_url("\"config_url\""); + int remaining_quotes = 1; + int url_start = -1; + for (int i = 0; i < html_data.size(); i++) { + if (substr_is(html_data, i, OTTdata)) { + for (int j = i + OTTdata.size(); j < html_data.size(); j++) { + if (substr_is(html_data, j, config_url)) { + for (int k = 0; k < html_data.size() - (i + OTTdata.size()); k++) { + char c = html_data[j + k + config_url.size()]; + if (remaining_quotes != 0) { + if (html_data[j + k + config_url.size()] == '"') { + remaining_quotes--; + } + continue; + } + else if (url_start == -1) { + url_start = j + k + config_url.size(); + } + + if (html_data[url_start + k] == '"') { + return html_data.substr(url_start, k); + } + } + } + } + } + } + return ""; + } + + std::string episode::get_episode_page(const std::string& url, const std::string& auth_cookie, const std::string& session_cookie, bool verbose) { + CURLcode ret; + CURL *hnd; + struct curl_slist *slist1; + + std::string episode_data; + + slist1 = NULL; + slist1 = curl_slist_append(slist1, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0"); + slist1 = curl_slist_append(slist1, "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"); + slist1 = curl_slist_append(slist1, "Accept-Language: en-US,en;q=0.5"); + slist1 = curl_slist_append(slist1, "Accept-Encoding: utf-8"); + slist1 = curl_slist_append(slist1, "DNT: 1"); + slist1 = curl_slist_append(slist1, "Connection: keep-alive"); + slist1 = curl_slist_append(slist1, ("Cookie: locale_det=en; referrer_url=https%3A%2F%2Fwww.dropout.tv%2Fgame-changer; _session=" + session_cookie + "; __stripe_mid=3dd96b43-2e51-411d-8614-9f052c92d8ba0506a7; _device=X11%3AFirefox%3A1u9pxwBcfaKsXubmTnNbfA; __cf_bm=" + auth_cookie + "; tracker=%7B%22country%22%3A%22us%22%2C%22platform%22%3A%22linux%22%2C%22uid%22%3A1048462031243%2C%22site_id%22%3A%2236348%22%7D").c_str()); + slist1 = curl_slist_append(slist1, "Upgrade-Insecure-Requests: 1"); + slist1 = curl_slist_append(slist1, "Sec-Fetch-Dest: document"); + slist1 = curl_slist_append(slist1, "Sec-Fetch-Mode: navigate"); + slist1 = curl_slist_append(slist1, "Sec-Fetch-Site: cross-site"); + slist1 = curl_slist_append(slist1, "Sec-GPC: 1"); + + hnd = curl_easy_init(); + curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L); + curl_easy_setopt(hnd, CURLOPT_URL, url.c_str()); + curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L); + curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1); + curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/7.84.0"); + curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L); + curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); + curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L); + curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L); + curl_easy_setopt(hnd, CURLOPT_VERBOSE, verbose); + + curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(hnd, CURLOPT_WRITEDATA, &episode_data); + /* Here is a list of options the curl code used that cannot get generated + as source easily. You may choose to either not use them or implement + them yourself. + + CURLOPT_WRITEDATA set to a objectpointer + CURLOPT_INTERLEAVEDATA set to a objectpointer + CURLOPT_WRITEFUNCTION set to a functionpointer + CURLOPT_READDATA set to a objectpointer + CURLOPT_READFUNCTION set to a functionpointer + CURLOPT_SEEKDATA set to a objectpointer + CURLOPT_SEEKFUNCTION set to a functionpointer + CURLOPT_ERRORBUFFER set to a objectpointer + CURLOPT_STDERR set to a objectpointer + CURLOPT_HEADERFUNCTION set to a functionpointer + CURLOPT_HEADERDATA set to a objectpointer + + */ + + ret = curl_easy_perform(hnd); + + curl_easy_cleanup(hnd); + hnd = NULL; + curl_slist_free_all(slist1); + slist1 = NULL; + + return episode_data; + } + + std::string episode::get_embedded_page(const std::string& url, const std::string& cookie, bool verbose) { + CURLcode ret; + CURL *hnd; + struct curl_slist *slist1; + std::string embedded_page; + + slist1 = NULL; + slist1 = curl_slist_append(slist1, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0"); + slist1 = curl_slist_append(slist1, "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"); + slist1 = curl_slist_append(slist1, "Accept-Language: en-US,en;q=0.5"); + slist1 = curl_slist_append(slist1, "Accept-Encoding: utf-8"); + slist1 = curl_slist_append(slist1, "DNT: 1"); + slist1 = curl_slist_append(slist1, "Connection: keep-alive"); + slist1 = curl_slist_append(slist1, "Referer: https://www.dropout.tv/"); + slist1 = curl_slist_append(slist1, "Cookie: __cf_bm=Ayc3uSgUEf9kJ20sfVBLgdo5fvloLmSLWBkJtzzhZR8-1662831290-0-ASVO2Fg9txI6nslt2tle7Y2MjRw4sI8/gFRbMDI8vHIP0nhb1SDk1I7lF5hWK9RMGP9wOFJwyqThLXQkuTj9m2c="); + slist1 = curl_slist_append(slist1, "Upgrade-Insecure-Requests: 1"); + slist1 = curl_slist_append(slist1, "Sec-Fetch-Dest: iframe"); + slist1 = curl_slist_append(slist1, "Sec-Fetch-Mode: navigate"); + slist1 = curl_slist_append(slist1, "Sec-Fetch-Site: cross-site"); + slist1 = curl_slist_append(slist1, "Sec-GPC: 1"); + + hnd = curl_easy_init(); + curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L); + curl_easy_setopt(hnd, CURLOPT_URL, url.c_str()); + curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L); + curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1); + curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/7.84.0"); + curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L); + curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); + curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L); + curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L); + curl_easy_setopt(hnd, CURLOPT_VERBOSE, verbose); + + + curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(hnd, CURLOPT_WRITEDATA, &embedded_page); + /* Here is a list of options the curl code used that cannot get generated + as source easily. You may choose to either not use them or implement + them yourself. + + CURLOPT_WRITEDATA set to a objectpointer + CURLOPT_INTERLEAVEDATA set to a objectpointer + CURLOPT_WRITEFUNCTION set to a functionpointer + CURLOPT_READDATA set to a objectpointer + CURLOPT_READFUNCTION set to a functionpointer + CURLOPT_SEEKDATA set to a objectpointer + CURLOPT_SEEKFUNCTION set to a functionpointer + CURLOPT_ERRORBUFFER set to a objectpointer + CURLOPT_STDERR set to a objectpointer + CURLOPT_HEADERFUNCTION set to a functionpointer + CURLOPT_HEADERDATA set to a objectpointer + + */ + + ret = curl_easy_perform(hnd); + + curl_easy_cleanup(hnd); + hnd = NULL; + curl_slist_free_all(slist1); + slist1 = NULL; + + return embedded_page; + } + + std::string episode::get_config_page(const std::string& url, bool verbose) { + CURLcode ret; + CURL *hnd; + struct curl_slist *slist1; + + std::string config_page; + + slist1 = NULL; + slist1 = curl_slist_append(slist1, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0"); + slist1 = curl_slist_append(slist1, "Accept: */*"); + slist1 = curl_slist_append(slist1, "Accept-Language: en-US,en;q=0.5"); + slist1 = curl_slist_append(slist1, "Accept-Encoding: utf-8"); + slist1 = curl_slist_append(slist1, "Referer: https://embed.vhx.tv/"); + slist1 = curl_slist_append(slist1, "Origin: https://embed.vhx.tv"); + slist1 = curl_slist_append(slist1, "DNT: 1"); + slist1 = curl_slist_append(slist1, "Connection: keep-alive"); + slist1 = curl_slist_append(slist1, "Sec-Fetch-Dest: empty"); + slist1 = curl_slist_append(slist1, "Sec-Fetch-Mode: cors"); + slist1 = curl_slist_append(slist1, "Sec-Fetch-Site: cross-site"); + slist1 = curl_slist_append(slist1, "Sec-GPC: 1"); + + hnd = curl_easy_init(); + curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L); + curl_easy_setopt(hnd, CURLOPT_URL, url.c_str()); + curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L); + curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1); + curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/7.84.0"); + curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L); + curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); + curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L); + curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L); + curl_easy_setopt(hnd, CURLOPT_VERBOSE, verbose); + + curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, WriteCallback); + curl_easy_setopt(hnd, CURLOPT_WRITEDATA, &config_page); + + /* Here is a list of options the curl code used that cannot get generated + as source easily. You may choose to either not use them or implement + them yourself. + + CURLOPT_WRITEDATA set to a objectpointer + CURLOPT_INTERLEAVEDATA set to a objectpointer + CURLOPT_WRITEFUNCTION set to a functionpointer + CURLOPT_READDATA set to a objectpointer + CURLOPT_READFUNCTION set to a functionpointer + CURLOPT_SEEKDATA set to a objectpointer + CURLOPT_SEEKFUNCTION set to a functionpointer + CURLOPT_ERRORBUFFER set to a objectpointer + CURLOPT_STDERR set to a objectpointer + CURLOPT_HEADERFUNCTION set to a functionpointer + CURLOPT_HEADERDATA set to a objectpointer + + */ + + ret = curl_easy_perform(hnd); + + curl_easy_cleanup(hnd); + hnd = NULL; + curl_slist_free_all(slist1); + slist1 = NULL; + + return config_page; + } + + + + std::string episode::get_video_url(const std::string& quality) { + int i = 0; + bool video_section = false; + for (; i < this->config_data.size(); i++ ) { + // std::cout << i << "/" << javascript_data.size() << ": " << javascript_data[i] << ": " << javascript_data.substr(i, 17) << ": " << video_section << "\n"; + if (this->config_data.substr(i, 9) == "video/mp4") { + video_section = true; + } + + if (video_section && this->config_data.substr(i, (R"("quality":")" + quality + '"').size()) == R"("quality":")" + quality + '"') { + break; + } + } + if (i == this->config_data.size()) { + std::cerr << "ERROR: quality of " << quality << " not found" << std::endl; + exit(7); + } + + std::string url; + for (; i > 0; i--) { + // std::cout << i << ": " << javascript_data[i] << ": " << javascript_data.substr(i-7, 7) << "\n"; + if (this->config_data.substr(i-7, 7) == R"("url":")") { + break; + } + } + + while (this->config_data[i] != '"') { + url += this->config_data[i++]; + } + return url; + } + + std::string episode::get_video_data(const std::string &quality) { + CURL* curl = curl_easy_init(); + CURLcode res; + if(curl) { + std::string out; + + curl_easy_setopt(curl, CURLOPT_URL, get_video_url(quality).c_str()); + curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, dropout_dl::episode::WriteCallback); + curl_easy_setopt(curl, CURLOPT_WRITEDATA, &out); + curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false); + curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, dropout_dl::curl_progress_func); + res = curl_easy_perform(curl); + curl_easy_cleanup(curl); + + return out; + } + return "CURL ERROR"; + } +} // dropout_dl \ No newline at end of file diff --git a/src/episode.h b/src/episode.h new file mode 100644 index 0000000..54d380e --- /dev/null +++ b/src/episode.h @@ -0,0 +1,146 @@ +// +// Created by moss on 9/28/22. +// +#pragma once + +#include +#include +#include +#include + +namespace dropout_dl { + + bool substr_is(const std::string& string, int start, const std::string& test_str); + + void replace_all(std::string& str, const std::string& from, const std::string& to); + + + #if defined(__WIN32__) + #include + msec_t time_ms(void); + #else + #include + long time_ms(); + #endif + + static int curl_progress_func(void* ptr, double total_to_download, double downloaded, double total_to_upload, double uploaded); + + class episode { + + public: + std::string series; + std::string name; + std::string episode_number; + std::string season_number; + std::string episode_url; + std::string episode_data; + std::string embedded_url; + std::string embedded_page_data; + std::string config_url; + std::string config_data; + std::string filename; + + // Curl + + static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) + { + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; + } + + static std::string get_episode_page(const std::string& url, const std::string& auth_cookie, const std::string& session_cookie, bool verbose = false); + + static std::string get_embedded_page(const std::string& url, const std::string& cookie, bool verbose = false); + + static std::string get_config_page(const std::string& url, bool verbose = false); + + + // Parsing + static std::string get_series_name(const std::string& html_data); + + static std::string get_episode_name(const std::string& html_data); + + static std::string get_episode_number(const std::string& html_data); + + static std::string get_season_number(const std::string& html_data); + + static std::string get_embed_url(const std::string& html_data); + + static std::string get_config_url(const std::string& html_data); + + std::string get_video_url(const std::string& quality); + + std::string get_video_data(const std::string& quality); + + + explicit episode(const std::string& episode_url, std::vector cookies, bool verbose = false) { + this->episode_url = episode_url; + + episode_data = get_episode_page(episode_url, cookies[0], cookies[1]); + + name = get_episode_name(episode_data); + + if (verbose) { + std::cout << "Got name: " << name << '\n'; + } + + this->season_number = get_season_number(episode_data); + + if (verbose) { + std::cout << "Got season: " << this->season_number << '\n'; + } + + this->episode_number = get_episode_number(episode_data); + + if (verbose) { + std::cout << "Got episode: " << this->episode_number << '\n'; + } + + this->series = get_series_name(episode_data); + + if (verbose) { + std::cout << "Got series: " << this->series << '\n'; + } + + std::replace(this->series.begin(), this->series.end(), ' ', '_'); + + std::replace(this->series.begin(), this->series.end(), ',', '_'); + + this->filename = this->series + "/S" + (this->season_number.size() < 2 ? "0" + this->season_number : this->season_number) + "E" + (this->episode_number.size() < 2 ? "0" + this->episode_number : this->episode_number) + this->name + ".mp4"; + + std::replace(filename.begin(), filename.end(), ' ', '_'); + + std::replace(filename.begin(), filename.end(), ',', '_'); + + if (verbose) { + std::cout << "filename: " << filename << '\n'; + } + + this->embedded_url = get_embed_url(episode_data); + + replace_all(this->embedded_url, "&", "&"); + + if (verbose) { + std::cout << "Got embedded url: " << this->embedded_url << '\n'; + } + + this->embedded_page_data = get_embedded_page(this->embedded_url, cookies[0]); + + if (this->embedded_page_data.find("you are not authorized") != std::string::npos) { + std::cerr << "ERROR: Could not access video. Try refreshing cookies.\n"; + exit(6); + } + + this->config_url = get_config_url(this->embedded_page_data); + + replace_all(this->config_url, "\\u0026", "&"); + + if (verbose) { + std::cout << "Got config url: " << this->embedded_url << '\n'; + } + + this->config_data = get_config_page(this->config_url); + } + }; + +} // dropout_dl diff --git a/src/main.cpp b/src/main.cpp index fcc8987..2196791 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,11 +1,8 @@ #include -#include -#include #include -#include #include -#include -#include + +#include "episode.h" #ifdef DROPOUT_DL_SQLITE #include @@ -15,456 +12,6 @@ #endif -static size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) -{ - ((std::string*)userp)->append((char*)contents, size * nmemb); - return size * nmemb; -} - -#if defined(__WIN32__) - -#include - -msec_t time_ms(void) -{ - return timeGetTime(); -} - -#else - -#include - -long time_ms() -{ - timeval tv; - gettimeofday(&tv, nullptr); - return tv.tv_sec * 1000 + tv.tv_usec / 1000; -} - -#endif - - -bool substr_is(const std::string& string, int start, const std::string& test_str) { - if (test_str.size() != test_str.size()) - return false; - - for (int i = start, j = 0; i < start + test_str.size(); i++, j++) { - if (string[i] != test_str[j]) { - return false; - } - } - return true; -} - -void replace_all(std::string& str, const std::string& from, const std::string& to) { - size_t start_pos = 0; - while((start_pos = str.find(from, start_pos)) != std::string::npos) { - str.replace(start_pos, from.length(), to); - start_pos += to.length(); // Handles case where 'to' is a substring of 'from' - } -} - -long current_time; -long last_progress_timestamp; -const double number_chars = 100; -const char* full_character = "▓"; -const char* empty_character = "░"; - - -int progress_func(void* ptr, double TotalToDownload, double NowDownloaded, double TotalToUpload, double NowUploaded) -{ - current_time = time_ms(); - if (current_time - 50 > last_progress_timestamp) { - double percent_done = (NowDownloaded / TotalToDownload) * number_chars; - double percent_done_clone = percent_done; - putchar('['); - while (percent_done_clone-- > 0) { - std::cout << full_character; - } - while (percent_done++ < number_chars) { - std::cout << empty_character; - } - putchar(']'); - putchar(' '); - std::cout << NowDownloaded / 1048576 << "MiB / " << TotalToDownload / 1048576 << "MiB "; - putchar('\r'); - last_progress_timestamp = time_ms(); - std::cout.flush(); - } - return 0; -} - -std::string get_series_name(const std::string& html_data) { - std::string series_title("series-title"); - std::string open_a_tag(""); - std::string close_a(""); - - int series_name_start = -1; - - for (int i = 0; i < html_data.size(); i++) { - if (substr_is(html_data, i, series_title)) { - for (int j = i + series_title.size(); j < html_data.size(); j++) { - if (html_data[j] == '\n' || html_data[j] == ' ' || html_data[j] == '\t') continue; - if (substr_is(html_data, j, open_a_tag)) { - for (int k = j + open_a_tag.size(); k < html_data.size(); k++) { - if (substr_is(html_data, k, close_tag)) { - for (int l = 0; l < html_data.size() - k; l++) { - char c = html_data[k + l]; - if (series_name_start == -1) { - if (html_data[k + l + 1] == '\n' || html_data[k + l + 1] == ' ' || - html_data[k + l + 1] == '\t') { - continue; - } else { - series_name_start = k + l + 1; - } - } - if (substr_is(html_data, k + l, close_a) || (series_name_start != -1 && html_data[k + l] == '\n')) { - return html_data.substr(series_name_start, l - (series_name_start - k)); - } - } - } - } - } - } - } - } - return "-1"; -} - -std::string get_episode_name(const std::string& html_data) { - int title_start = -1; - std::string video_title("video-title"); - std::string open_strong(""); - std::string close_strong(""); - for (int i = 0; i < html_data.size(); i++) { - if (substr_is(html_data, i, video_title)) { - for (int j = i; j < html_data.size(); j++) { - if (substr_is(html_data, j, open_strong)) { - title_start = j + 8; - break; - } - } - for (int j = 0; j < html_data.size() - title_start; j++) { - if (substr_is(html_data, title_start + j, close_strong)) { - return html_data.substr(title_start, j); - } - } - } - } - return "ERROR"; -} - -std::string get_episode_number(const std::string& html_data) { - std::string episode("Episode"); - std::string close_a(""); - std::string episode_num; - for (int i = 0; i < html_data.size(); i++) { - if (substr_is(html_data, i, episode)) { - for (int j = i + 8; j < html_data.size(); j++) { - if (html_data[j] == '\n' || html_data[j] == ' ' || html_data[j] == '\t') continue; - if (substr_is(html_data, j, close_a)) { - return episode_num; - } - episode_num += html_data[j]; - } - } - } - return "-1"; -} - -std::string get_season_number(const std::string& html_data) { - std::string season("Season"); - std::string dash(","); - std::string season_num; - for (int i = 0; i < html_data.size(); i++) { - if (substr_is(html_data, i, season)) { - for (int j = i + 7; j < html_data.size(); j++) { - if (html_data[j] == '\n' || html_data[j] == ' ' || html_data[j] == '\t') continue; - if (html_data[j] == '-' || html_data[j] == ',' ) { - return season_num; - } - season_num += html_data[j]; - } - } - } - return "-1"; -} - -std::string get_embed_url(const std::string& html_data) { - std::string config("window.VHX.config"); - std::string embed_url("embed_url: "); - for (int i = 0; i < html_data.size(); i++) { - if (substr_is(html_data, i, config)) { - for (int j = i + config.size(); j < html_data.size(); j++) { - if (substr_is(html_data, j, embed_url)) { - for (int k = 0; k < html_data.size(); k++) { - if (html_data[k + j + embed_url.size() + 1] == '"') { - return html_data.substr(j + embed_url.size() + 1, k); - } - } - } - } - } - } - return ""; -} - -std::string get_config_url(const std::string& html_data) { - std::string OTTdata("OTTData"); - std::string config_url("\"config_url\""); - int remaining_quotes = 1; - int url_start = -1; - for (int i = 0; i < html_data.size(); i++) { - if (substr_is(html_data, i, OTTdata)) { - for (int j = i + OTTdata.size(); j < html_data.size(); j++) { - if (substr_is(html_data, j, config_url)) { - for (int k = 0; k < html_data.size() - (i + OTTdata.size()); k++) { - char c = html_data[j + k + config_url.size()]; - if (remaining_quotes != 0) { - if (html_data[j + k + config_url.size()] == '"') { - remaining_quotes--; - } - continue; - } - else if (url_start == -1) { - url_start = j + k + config_url.size(); - } - - if (html_data[url_start + k] == '"') { - return html_data.substr(url_start, k); - } - } - } - } - } - } - return ""; -} - -std::string get_video_url(const std::string& config_data, const std::string& quality) { - int i = 0; - bool video_section = false; - for (; i < config_data.size(); i++ ) { - // std::cout << i << "/" << javascript_data.size() << ": " << javascript_data[i] << ": " << javascript_data.substr(i, 17) << ": " << video_section << "\n"; - if (config_data.substr(i, 9) == "video/mp4") { - video_section = true; - } - - if (video_section && config_data.substr(i, (R"("quality":")" + quality + '"').size()) == R"("quality":")" + quality + '"') { - break; - } - } - if (i == config_data.size()) { - std::cerr << "ERROR: quality of " << quality << " not found" << std::endl; - exit(7); - } - - std::string url; - for (; i > 0; i--) { - // std::cout << i << ": " << javascript_data[i] << ": " << javascript_data.substr(i-7, 7) << "\n"; - if (config_data.substr(i-7, 7) == R"("url":")") { - break; - } - } - - while (config_data[i] != '"') { - url += config_data[i++]; - } - return url; -} - -std::string get_episode_page(const std::string& url, const std::string& auth_cookie, const std::string& session_cookie, bool verbose = false) { - CURLcode ret; - CURL *hnd; - struct curl_slist *slist1; - - std::string episode_data; - - slist1 = NULL; - slist1 = curl_slist_append(slist1, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0"); - slist1 = curl_slist_append(slist1, "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"); - slist1 = curl_slist_append(slist1, "Accept-Language: en-US,en;q=0.5"); - slist1 = curl_slist_append(slist1, "Accept-Encoding: utf-8"); - slist1 = curl_slist_append(slist1, "DNT: 1"); - slist1 = curl_slist_append(slist1, "Connection: keep-alive"); - slist1 = curl_slist_append(slist1, ("Cookie: locale_det=en; referrer_url=https%3A%2F%2Fwww.dropout.tv%2Fgame-changer; _session=" + session_cookie + "; __stripe_mid=3dd96b43-2e51-411d-8614-9f052c92d8ba0506a7; _device=X11%3AFirefox%3A1u9pxwBcfaKsXubmTnNbfA; __cf_bm=" + auth_cookie + "; tracker=%7B%22country%22%3A%22us%22%2C%22platform%22%3A%22linux%22%2C%22uid%22%3A1048462031243%2C%22site_id%22%3A%2236348%22%7D").c_str()); - slist1 = curl_slist_append(slist1, "Upgrade-Insecure-Requests: 1"); - slist1 = curl_slist_append(slist1, "Sec-Fetch-Dest: document"); - slist1 = curl_slist_append(slist1, "Sec-Fetch-Mode: navigate"); - slist1 = curl_slist_append(slist1, "Sec-Fetch-Site: cross-site"); - slist1 = curl_slist_append(slist1, "Sec-GPC: 1"); - - hnd = curl_easy_init(); - curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L); - curl_easy_setopt(hnd, CURLOPT_URL, url.c_str()); - curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1); - curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/7.84.0"); - curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L); - curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); - curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L); - curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L); - curl_easy_setopt(hnd, CURLOPT_VERBOSE, verbose); - - curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(hnd, CURLOPT_WRITEDATA, &episode_data); - /* Here is a list of options the curl code used that cannot get generated - as source easily. You may choose to either not use them or implement - them yourself. - - CURLOPT_WRITEDATA set to a objectpointer - CURLOPT_INTERLEAVEDATA set to a objectpointer - CURLOPT_WRITEFUNCTION set to a functionpointer - CURLOPT_READDATA set to a objectpointer - CURLOPT_READFUNCTION set to a functionpointer - CURLOPT_SEEKDATA set to a objectpointer - CURLOPT_SEEKFUNCTION set to a functionpointer - CURLOPT_ERRORBUFFER set to a objectpointer - CURLOPT_STDERR set to a objectpointer - CURLOPT_HEADERFUNCTION set to a functionpointer - CURLOPT_HEADERDATA set to a objectpointer - - */ - - ret = curl_easy_perform(hnd); - - curl_easy_cleanup(hnd); - hnd = NULL; - curl_slist_free_all(slist1); - slist1 = NULL; - - return episode_data; -} - -std::string get_embedded_page(const std::string& url, const std::string& cookie, bool verbose = false) { - CURLcode ret; - CURL *hnd; - struct curl_slist *slist1; - std::string embedded_page; - - slist1 = NULL; - slist1 = curl_slist_append(slist1, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0"); - slist1 = curl_slist_append(slist1, "Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8"); - slist1 = curl_slist_append(slist1, "Accept-Language: en-US,en;q=0.5"); - slist1 = curl_slist_append(slist1, "Accept-Encoding: utf-8"); - slist1 = curl_slist_append(slist1, "DNT: 1"); - slist1 = curl_slist_append(slist1, "Connection: keep-alive"); - slist1 = curl_slist_append(slist1, "Referer: https://www.dropout.tv/"); - slist1 = curl_slist_append(slist1, "Cookie: __cf_bm=Ayc3uSgUEf9kJ20sfVBLgdo5fvloLmSLWBkJtzzhZR8-1662831290-0-ASVO2Fg9txI6nslt2tle7Y2MjRw4sI8/gFRbMDI8vHIP0nhb1SDk1I7lF5hWK9RMGP9wOFJwyqThLXQkuTj9m2c="); - slist1 = curl_slist_append(slist1, "Upgrade-Insecure-Requests: 1"); - slist1 = curl_slist_append(slist1, "Sec-Fetch-Dest: iframe"); - slist1 = curl_slist_append(slist1, "Sec-Fetch-Mode: navigate"); - slist1 = curl_slist_append(slist1, "Sec-Fetch-Site: cross-site"); - slist1 = curl_slist_append(slist1, "Sec-GPC: 1"); - - hnd = curl_easy_init(); - curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L); - curl_easy_setopt(hnd, CURLOPT_URL, url.c_str()); - curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1); - curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/7.84.0"); - curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L); - curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); - curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L); - curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L); - curl_easy_setopt(hnd, CURLOPT_VERBOSE, verbose); - - - curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(hnd, CURLOPT_WRITEDATA, &embedded_page); - /* Here is a list of options the curl code used that cannot get generated - as source easily. You may choose to either not use them or implement - them yourself. - - CURLOPT_WRITEDATA set to a objectpointer - CURLOPT_INTERLEAVEDATA set to a objectpointer - CURLOPT_WRITEFUNCTION set to a functionpointer - CURLOPT_READDATA set to a objectpointer - CURLOPT_READFUNCTION set to a functionpointer - CURLOPT_SEEKDATA set to a objectpointer - CURLOPT_SEEKFUNCTION set to a functionpointer - CURLOPT_ERRORBUFFER set to a objectpointer - CURLOPT_STDERR set to a objectpointer - CURLOPT_HEADERFUNCTION set to a functionpointer - CURLOPT_HEADERDATA set to a objectpointer - - */ - - ret = curl_easy_perform(hnd); - - curl_easy_cleanup(hnd); - hnd = NULL; - curl_slist_free_all(slist1); - slist1 = NULL; - - return embedded_page; - } - -std::string get_config_page(const std::string& url, bool verbose = false) { - CURLcode ret; - CURL *hnd; - struct curl_slist *slist1; - - std::string config_page; - - slist1 = NULL; - slist1 = curl_slist_append(slist1, "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:101.0) Gecko/20100101 Firefox/101.0"); - slist1 = curl_slist_append(slist1, "Accept: */*"); - slist1 = curl_slist_append(slist1, "Accept-Language: en-US,en;q=0.5"); - slist1 = curl_slist_append(slist1, "Accept-Encoding: utf-8"); - slist1 = curl_slist_append(slist1, "Referer: https://embed.vhx.tv/"); - slist1 = curl_slist_append(slist1, "Origin: https://embed.vhx.tv"); - slist1 = curl_slist_append(slist1, "DNT: 1"); - slist1 = curl_slist_append(slist1, "Connection: keep-alive"); - slist1 = curl_slist_append(slist1, "Sec-Fetch-Dest: empty"); - slist1 = curl_slist_append(slist1, "Sec-Fetch-Mode: cors"); - slist1 = curl_slist_append(slist1, "Sec-Fetch-Site: cross-site"); - slist1 = curl_slist_append(slist1, "Sec-GPC: 1"); - - hnd = curl_easy_init(); - curl_easy_setopt(hnd, CURLOPT_BUFFERSIZE, 102400L); - curl_easy_setopt(hnd, CURLOPT_URL, url.c_str()); - curl_easy_setopt(hnd, CURLOPT_NOPROGRESS, 1L); - curl_easy_setopt(hnd, CURLOPT_HTTPHEADER, slist1); - curl_easy_setopt(hnd, CURLOPT_USERAGENT, "curl/7.84.0"); - curl_easy_setopt(hnd, CURLOPT_MAXREDIRS, 50L); - curl_easy_setopt(hnd, CURLOPT_HTTP_VERSION, (long)CURL_HTTP_VERSION_2TLS); - curl_easy_setopt(hnd, CURLOPT_FTP_SKIP_PASV_IP, 1L); - curl_easy_setopt(hnd, CURLOPT_TCP_KEEPALIVE, 1L); - curl_easy_setopt(hnd, CURLOPT_VERBOSE, verbose); - - curl_easy_setopt(hnd, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(hnd, CURLOPT_WRITEDATA, &config_page); - - /* Here is a list of options the curl code used that cannot get generated - as source easily. You may choose to either not use them or implement - them yourself. - - CURLOPT_WRITEDATA set to a objectpointer - CURLOPT_INTERLEAVEDATA set to a objectpointer - CURLOPT_WRITEFUNCTION set to a functionpointer - CURLOPT_READDATA set to a objectpointer - CURLOPT_READFUNCTION set to a functionpointer - CURLOPT_SEEKDATA set to a objectpointer - CURLOPT_SEEKFUNCTION set to a functionpointer - CURLOPT_ERRORBUFFER set to a objectpointer - CURLOPT_STDERR set to a objectpointer - CURLOPT_HEADERFUNCTION set to a functionpointer - CURLOPT_HEADERDATA set to a objectpointer - - */ - - ret = curl_easy_perform(hnd); - - curl_easy_cleanup(hnd); - hnd = NULL; - curl_slist_free_all(slist1); - slist1 = NULL; - - return config_page; -} - static int sqlite_write_callback(void* data, int argc, char** argv, char** azColName) { if (argc < 1) { @@ -763,26 +310,13 @@ int main(int argc, char** argv) { bool verbose = true; std::string quality = "1080p"; - std::string series_name; - std::string name; - std::string filename; - std::string season; - std::string episode; - std::string firefox_profile; std::string chrome_profile; - std::string config_url; - std::string embed_url; std::string episode_url; - std::vector cookies = get_cookies(verbose); + std::vector cookies; - CURL *curl; - CURLcode res; - std::string episode_data; - std::string embedded_data; - std::string config_data; std::string video_data; if (argc > 1) { @@ -796,106 +330,26 @@ int main(int argc, char** argv) { std::cin >> episode_url; } - curl = curl_easy_init(); - if(curl) { + cookies = get_cookies(verbose); - episode_data = get_episode_page(episode_url, cookies[0], cookies[1]); + dropout_dl::episode ep(episode_url, cookies, verbose); - name = get_episode_name(episode_data); - - if (verbose) { - std::cout << "Got name: " << name << '\n'; - } - - season = get_season_number(episode_data); - - if (verbose) { - std::cout << "Got season: " << season << '\n'; - } - - episode = get_episode_number(episode_data); - - if (verbose) { - std::cout << "Got episode: " << episode << '\n'; - } - - series_name = get_series_name(episode_data); - - if (verbose) { - std::cout << "Got series: " << series_name << '\n'; - } - - std::replace(series_name.begin(), series_name.end(), ' ', '_'); - - std::replace(series_name.begin(), series_name.end(), ',', '_'); - - filename = series_name + "/S" + (season.size() < 2 ? "0" + season : season) + "E" + (episode.size() < 2 ? "0" + episode : episode) + name + ".mp4"; - - std::replace(filename.begin(), filename.end(), ' ', '_'); - - std::replace(filename.begin(), filename.end(), ',', '_'); - - if (verbose) { - std::cout << "filename: " << filename << '\n'; - } - - embed_url = get_embed_url(episode_data); - - replace_all(embed_url, "&", "&"); - - if (verbose) { - std::cout << "Got embedded url: " << embed_url << '\n'; - } - } - - curl = curl_easy_init(); - if (curl) { - embedded_data = get_embedded_page(embed_url, cookies[0]); - - if (embedded_data.find("you are not authorized") != std::string::npos) { - std::cerr << "ERROR: Could not access video. Try refreshing cookies.\n"; - return 6; - } - - config_url = get_config_url(embedded_data); - - replace_all(config_url, "\\u0026", "&"); - - if (verbose) { - std::cout << "Got config url: " << embed_url << '\n'; - } - - config_data = get_config_page(config_url); - } - - std::string video_url = get_video_url(config_data, quality); + std::string video_url = ep.get_video_url(quality); if (verbose) { std::cout << "Got video url: " << video_url << '\n'; } - curl = curl_easy_init(); - if(curl) { - if (!std::filesystem::is_directory(series_name)) { - std::filesystem::create_directories(series_name); - if (verbose) { - std::cout << "Creating series directory" << '\n'; - } + + if (!std::filesystem::is_directory(ep.series)) { + std::filesystem::create_directories(ep.series); + if (verbose) { + std::cout << "Creating series directory" << '\n'; } - - std::fstream out(filename, std::ios_base::in|std::ios_base::out|std::ios_base::trunc); - - curl_easy_setopt(curl, CURLOPT_URL, video_url.c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, WriteCallback); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, &video_data); - curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false); - curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, progress_func); - std::cout << "Getting " << filename << '\n'; - res = curl_easy_perform(curl); - curl_easy_cleanup(curl); - - putchar('\n'); - - out << video_data << std::endl; } + + std::fstream out(ep.filename, std::ios_base::in|std::ios_base::out|std::ios_base::trunc); + + out << ep.get_video_data(quality) << std::endl; + return 0; }