From 1138e0fc3f5a771fef49f9b82430b92b36accb83 Mon Sep 17 00:00:00 2001 From: Moss Date: Thu, 29 Sep 2022 03:04:57 -0400 Subject: [PATCH] Options: Added '--series' Option This allows for an entire series to be downloaded at once. There are issues with filenames but it works mostly. --- CMakeLists.txt | 2 +- readme.md | 60 ++++++++-------- src/episode.cpp | 76 +++++++++++--------- src/episode.h | 27 +++---- src/main.cpp | 188 ++++++++++++++++++++++++++---------------------- src/season.cpp | 60 ++++++++++++++++ src/season.h | 33 +++++++++ src/series.cpp | 179 +++++++++++++++++++++++++++++++++++++++++++++ src/series.h | 36 ++++++++++ 9 files changed, 497 insertions(+), 164 deletions(-) create mode 100644 src/season.cpp create mode 100644 src/season.h create mode 100644 src/series.cpp create mode 100644 src/series.h diff --git a/CMakeLists.txt b/CMakeLists.txt index f6bd781..5908759 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -3,7 +3,7 @@ project(dropout-dl) set(CMAKE_CXX_STANDARD 17) -add_executable(dropout-dl src/episode.cpp src/main.cpp) +add_executable(dropout-dl src/episode.cpp src/season.cpp src/series.cpp src/main.cpp) IF (EXISTS "firefox_profile") target_link_libraries(dropout-dl curl sqlite3) diff --git a/readme.md b/readme.md index 239dbcd..e1af34b 100644 --- a/readme.md +++ b/readme.md @@ -9,47 +9,45 @@ cd make ``` -## setup -the sqlite and curl libraries are required \ -additionally [cookies](#cookies) are needed +### Dependency Installation +sqlite-devel is optional but highly recommended. -### dependency installation -#### void linux +#### Void ``` -sudo xbps-install -S libcurl +sudo xbps-install -S libcurl sqlite-devel ``` -#### debian +#### Debian ``` -sudo apt install libcurl +sudo apt install libcurl sqlite-devel ``` ## cookies -### firefox -#### option 1 (requires sqlite-devel) -create a file named `firefox_profile` in the build directory and paste in your [firefox profile folder path](https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data) -#### option 2 (requires sqlite) -close firefox and go to your [firefox profile folder](https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data) +### Firefox +#### Option 1 (requires sqlite-devel) +Create a file named `firefox_profile` in the build directory and paste in your [firefox profile folder path](https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data) +#### Option 2 (requires sqlite) +Close firefox and go to your [firefox profile folder](https://support.mozilla.org/en-US/kb/profiles-where-firefox-stores-user-data) ``` sqlite3 cookies.sqlite "SELECT value FROM moz_cookies WHERE host LIKE '%dropout.tv%' AND name='__cf_bm';" > /auth_cookie sqlite3 cookies.sqlite "SELECT value FROM moz_cookies WHERE host LIKE '%dropout.tv%' AND name='_session';" > /session_cookie ``` -this needs to be redone every time the cookies expire (~30 minutes) -#### option 3 -open firefox and go to any dropout.tv episode \ -open the dev tools and go to network then refresh \ -search for `?api` and select the top request \ -copy the `__cf_bm` cookie from the cookies section \ -create a file called `auth_cookie` and paste the cookie in the file \ -go back to firefox and copy the `_session` cookie into a file named `session_cookie` \ -this needs to be redone everytime the cookie expires (~30 minutes) +This needs to be redone every time the cookies expire (~30 minutes) +#### Option 3 +Open firefox and go to any dropout.tv episode \ +Open the dev tools and go to network then refresh \ +Search for `?api` and select the top request \ +Copy the `__cf_bm` cookie from the cookies section \ +Create a file called `auth_cookie` and paste the cookie in the file \ +Go back to firefox and copy the `_session` cookie into a file named `session_cookie` \ +This needs to be redone everytime the cookie expires (~30 minutes) ### chrome -#### option 1 (requires sqlite-devel and libgcrypt) NOT CURRENTLY FUNCTIONAL -create a file named `chrome_profile` in the build directory and paste in your chrome profile folder path (found on [chrome://version](chrome://version)) -#### option 2 -go to settings > privacy and security > cookies > see all cookies > vhx.tv > __cf_bm \ -copy the `content` and paste it into the `cookie` file \ -this needs to be redone every time the cookies expire (~30 minutes) +#### Option 1 (requires sqlite-devel and libgcrypt) NOT CURRENTLY FUNCTIONAL +Create a file named `chrome_profile` in the build directory and paste in your chrome profile folder path (found on [chrome://version](chrome://version)) +#### Option 2 +Go to settings > privacy and security > cookies > see all cookies > vhx.tv > __cf_bm \ +Copy the `content` and paste it into the `cookie` file \ +This needs to be redone every time the cookies expire (~30 minutes) ## How to Use ``` @@ -65,6 +63,8 @@ this needs to be redone every time the cookies expire (~30 minutes) --output-directory Set the directory where files are output --verbose Display debug information while running --force-cookies Interpret the next to arguments as authentication cookie and session cookie +--series Interpret the url as a link to a series and download all episodes from all seasons +--episode Select an episode from the series to download +--season Select a season from the series to download ``` -dropout-dl will download the episode into a folder with the name of the series in the format -`SE.mp4` \ No newline at end of file +By default, dropout-dl will download the episode in the format `/SE.mp4` \ No newline at end of file diff --git a/src/episode.cpp b/src/episode.cpp index ccc85a1..ae73cd3 100644 --- a/src/episode.cpp +++ b/src/episode.cpp @@ -27,15 +27,6 @@ namespace dropout_dl { } } - bool contains(const std::string& string, const std::string& test_str) { - for (int i = 0; i < string.size() - test_str.size(); i++) { - if (string.substr(i, test_str.size()) == test_str) { - return true; - } - } - return false; - } - #if defined(__WIN32__) #include msec_t time_ms(void) @@ -82,6 +73,12 @@ namespace dropout_dl { return 0; } + size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp) + { + ((std::string*)userp)->append((char*)contents, size * nmemb); + return size * nmemb; + } + // episode statics std::string episode::get_series_name(const std::string& html_data) { std::string series_title("series-title"); @@ -358,14 +355,13 @@ namespace dropout_dl { return embedded_page; } - std::string episode::get_config_page(const std::string& url, bool verbose) { - CURLcode ret; + std::string get_generic_page(const std::string& url, bool verbose) { CURL *hnd; struct curl_slist *slist1; std::string config_page; - slist1 = NULL; + slist1 = nullptr; 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"); @@ -394,30 +390,12 @@ namespace dropout_dl { 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_perform(hnd); curl_easy_cleanup(hnd); - hnd = NULL; + hnd = nullptr; curl_slist_free_all(slist1); - slist1 = NULL; + slist1 = nullptr; return config_page; } @@ -468,7 +446,6 @@ namespace dropout_dl { return qualities; } - std::string episode::get_video_url(const std::string& quality) { for (int i = 0; i < qualities.size(); i++) { if (qualities[i] == quality) { @@ -492,7 +469,7 @@ namespace dropout_dl { 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_WRITEFUNCTION, dropout_dl::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); @@ -503,4 +480,33 @@ namespace dropout_dl { } return "CURL ERROR"; } + + + void episode::download(const std::string& quality, const std::string& series_directory, std::string filename) { + if (filename.empty()) { + filename = "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"; + } + + if (quality == "all") { + for (const auto &possible_quality: this->qualities) { + if (!std::filesystem::is_directory(series_directory + "/" + possible_quality)) { + std::filesystem::create_directories(series_directory + "/" + possible_quality); + if (this->verbose) { + std::cout << "Creating quality directory" << '\n'; + } + } + std::fstream out(series_directory + "/" + possible_quality + "/" + filename, + std::ios_base::in | std::ios_base::out | std::ios_base::trunc); + + out << this->get_video_data(possible_quality) << std::endl; + } + } else { + std::fstream out(series_directory + "/" + filename, + std::ios_base::in | std::ios_base::out | std::ios_base::trunc); + + out << this->get_video_data(quality) << std::endl; + } + } } // dropout_dl \ No newline at end of file diff --git a/src/episode.h b/src/episode.h index 1cf4c34..ce3eb3b 100644 --- a/src/episode.h +++ b/src/episode.h @@ -7,6 +7,8 @@ #include #include #include +#include +#include namespace dropout_dl { @@ -14,8 +16,6 @@ namespace dropout_dl { void replace_all(std::string& str, const std::string& from, const std::string& to); - bool contains(const std::string& string, const std::string& test_str); - #if defined(__WIN32__) #include msec_t time_ms(void); @@ -26,6 +26,10 @@ namespace dropout_dl { static int curl_progress_func(void* ptr, double total_to_download, double downloaded, double total_to_upload, double uploaded); + size_t WriteCallback(void *contents, size_t size, size_t nmemb, void *userp); + + std::string get_generic_page(const std::string& url, bool verbose = false); + class episode { public: @@ -42,22 +46,14 @@ namespace dropout_dl { std::vector qualities; std::vector quality_urls; - bool verbose; + bool verbose = false; // 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); @@ -78,8 +74,11 @@ namespace dropout_dl { std::string get_video_data(const std::string& quality); + void download(const std::string& quality, const std::string& series_directory, std::string filename = ""); + + episode(const std::string& episode_url, std::vector cookies, bool verbose = false) { + std::cout << episode_url << std::endl; - explicit episode(const std::string& episode_url, std::vector cookies, bool verbose = false) { this->episode_url = episode_url; this->verbose = verbose; @@ -141,10 +140,12 @@ namespace dropout_dl { std::cout << "Got config url: " << this->embedded_url << '\n'; } - this->config_data = get_config_page(this->config_url); + this->config_data = get_generic_page(this->config_url); this->get_qualities(); } + + episode() = default; }; } // dropout_dl diff --git a/src/main.cpp b/src/main.cpp index 748ff4b..88282c5 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,8 +1,6 @@ #include -#include -#include -#include "episode.h" +#include "series.h" #ifdef DROPOUT_DL_SQLITE #include @@ -320,9 +318,12 @@ public: std::string url; bool verbose = false; bool cookies_forced = false; + bool series = false; std::string quality; std::string filename; std::string series_dir; + std::string episode; + std::string season; std::vector cookies; static std::vector convert_program_args(int argc, char** argv) { @@ -337,59 +338,79 @@ public: std::vector args = convert_program_args(argc, argv); for (int i = 0; i < args.size(); i++) { - const auto& arg = args[i]; + std::string arg = args[i]; if (arg.substr(0, 2) != "--") { url = arg; + continue; } - else { - if (arg == "--verbose") { - verbose = true; - } else if (arg == "--quality") { - if (i + 1 >= args.size()) { - std::cerr << "ARGUMENT PARSE ERROR: --quality used with too few following arguments\n"; - exit(8); - } - quality = args[++i]; + arg = arg.substr(2); + if (arg == "verbose") { + verbose = true; + } else if (arg == "quality") { + if (i + 1 >= args.size()) { + std::cerr << "ARGUMENT PARSE ERROR: --quality used with too few following arguments\n"; + exit(8); } - else if (arg == "--force-cookies") { - if (i + 2 >= args.size()) { - std::cerr << "ARGUMENT PARSE ERROR: --force-cookies used with too few following arguments\n"; - exit(8); - } - cookies.emplace_back(args[++i]); - cookies.emplace_back(args[++i]); - cookies_forced = true; + quality = args[++i]; + } + else if (arg == "force-cookies") { + if (i + 2 >= args.size()) { + std::cerr << "ARGUMENT PARSE ERROR: --force-cookies used with too few following arguments\n"; + exit(8); } - else if (arg == "--output") { - if (i + 1 >= args.size()) { - std::cerr << "ARGUMENT PARSE ERROR: --output used with too few following arguments\n"; - exit(8); - } - filename = args[++i]; + cookies.emplace_back(args[++i]); + cookies.emplace_back(args[++i]); + cookies_forced = true; + } + else if (arg == "output") { + if (i + 1 >= args.size()) { + std::cerr << "ARGUMENT PARSE ERROR: --output used with too few following arguments\n"; + exit(8); } - else if (arg == "--output-directory") { - if (i + 1 >= args.size()) { - std::cerr << "ARGUMENT PARSE ERROR: --output-directory used with too few following arguments\n"; - exit(8); - } - series_dir = args[++i]; + filename = args[++i]; + } + else if (arg == "output-directory") { + if (i + 1 >= args.size()) { + std::cerr << "ARGUMENT PARSE ERROR: --output-directory used with too few following arguments\n"; + exit(8); } - else if (arg == "--help") { - std::cout << "Usage: dropout-dl [OPTIONS] [OPTIONS]\n" - "\n" - "Options:\n" - "\t--help Display this message\n" - "\t--quality Set the quality of the downloaded video. Quality can be set to 'all' which\n" - "\t will download all qualities and place them into separate folders\n" - "\t--output Set the output filename\n" - "\t--output-directory Set the directory where files are output\n" - "\t--verbose Display debug information while running\n" - "\t--force-cookies Interpret the next to arguments as authentication cookie and session cookie\n" - << std::endl; + series_dir = args[++i]; + } + else if (arg == "series") { + series = true; + } + else if (arg == "episode") { + if (i + 1 >= args.size()) { + std::cerr << "ARGUMENT PARSE ERROR: --episode used with too few following arguments\n"; + exit(8); + } + episode = args[++i]; + } + else if (arg == "season") { + if (i + 1 >= args.size()) { + std::cerr << "ARGUMENT PARSE ERROR: --season used with too few following arguments\n"; + exit(8); + } + season = args[++i]; + } + else if (arg == "help") { + std::cout << "Usage: dropout-dl [OPTIONS] [OPTIONS]\n" + "\n" + "Options:\n" + "\t--help Display this message\n" + "\t--quality Set the quality of the downloaded video. Quality can be set to 'all' which\n" + "\t will download all qualities and place them into separate folders\n" + "\t--output Set the output filename\n" + "\t--output-directory Set the directory where files are output\n" + "\t--verbose Display debug information while running\n" + "\t--force-cookies Interpret the next to arguments as authentication cookie and session cookie\n" + "\t--series Interpret the url as a link to a series and download all episodes from all seasons\n" + "\t--episode Select an episode from the series to download\n" + "\t--season Select a season from the series to download\n" + << std::endl; - exit(0); - } + exit(0); } } @@ -401,7 +422,6 @@ public: int main(int argc, char** argv) { - options options(argc, argv); std::cout << "quality: " << options.quality << std::endl; @@ -425,52 +445,50 @@ int main(int argc, char** argv) { options.cookies = get_cookies(options.verbose); } - dropout_dl::episode ep(options.url, options.cookies, options.verbose); + if (options.series) { + dropout_dl::series series(options.url, options.cookies); - if (options.filename.empty()) { - options.filename = "S" + (ep.season_number.size() < 2 ? "0" + ep.season_number : ep.season_number) + "E" + - (ep.episode_number.size() < 2 ? "0" + ep.episode_number : ep.episode_number) + ep.name + - ".mp4"; + if (options.series_dir.empty()) { + options.series_dir = series.name; - std::replace(options.filename.begin(), options.filename.end(), ' ', '_'); + std::replace(options.series_dir.begin(), options.series_dir.end(), ' ', '_'); - std::replace(options.filename.begin(), options.filename.end(), ',', '_'); - } - - if (options.verbose) { - std::cout << "filename: " << options.filename << '\n'; - } - - if (options.series_dir.empty()) { - options.series_dir = ep.series; - } - - if (!std::filesystem::is_directory(ep.series)) { - std::filesystem::create_directories(ep.series); - if (options.verbose) { - std::cout << "Creating series directory" << '\n'; + std::replace(options.series_dir.begin(), options.series_dir.end(), ',', '_'); } - } - if (options.quality == "all") { - for (const auto& possible_quality : ep.qualities) { - if (!std::filesystem::is_directory(options.series_dir + "/" + possible_quality)) { - std::filesystem::create_directories(options.series_dir + "/" + possible_quality); - if (options.verbose) { - std::cout << "Creating series directory" << '\n'; - } - } - std::fstream out(options.series_dir + "/" + possible_quality + "/" + options.filename, - std::ios_base::in | std::ios_base::out | std::ios_base::trunc); - - out << ep.get_video_data(possible_quality) << std::endl; - } + series.download(options.quality, options.series_dir); } else { - std::fstream out(options.series_dir + "/" + options.filename, std::ios_base::in | std::ios_base::out | std::ios_base::trunc); + dropout_dl::episode ep(options.url, options.cookies, options.verbose); - out << ep.get_video_data(options.quality) << std::endl; + if (options.filename.empty()) { + options.filename = "S" + (ep.season_number.size() < 2 ? "0" + ep.season_number : ep.season_number) + "E" + + (ep.episode_number.size() < 2 ? "0" + ep.episode_number : ep.episode_number) + ep.name + + ".mp4"; + + std::replace(options.filename.begin(), options.filename.end(), ' ', '_'); + + std::replace(options.filename.begin(), options.filename.end(), ',', '_'); + } + + if (options.verbose) { + std::cout << "filename: " << options.filename << '\n'; + } + + if (options.series_dir.empty()) { + options.series_dir = ep.series; + } + + if (!std::filesystem::is_directory(ep.series)) { + std::filesystem::create_directories(ep.series); + if (options.verbose) { + std::cout << "Creating series directory" << '\n'; + } + } + + ep.download(options.quality, options.series_dir, options.filename); } + return 0; } diff --git a/src/season.cpp b/src/season.cpp new file mode 100644 index 0000000..c0dd438 --- /dev/null +++ b/src/season.cpp @@ -0,0 +1,60 @@ +// +// Created by moss on 9/29/22. +// + +#include "season.h" + +namespace dropout_dl { + episode get_episode(const std::string& html_data, int& start_point, const std::vector& cookies) { + int link_start = 0; + for (int i = start_point; i > 0; i--) { + if (substr_is(html_data, i, " season::get_episodes(const std::string &html_data, const std::vector& cookies) { + std::vector out; + + std::string site_video(R"(class="browse-item-link" data-track-event="site_video")"); + + + for (int i = 0; i < html_data.size(); i++) { + if (substr_is(html_data, i, site_video)) { + episode e = get_episode(html_data, i, cookies); + if (e.episode_url.empty()) { + continue; + } + out.push_back(e); + } + } + + return out; + } + + void season::download(const std::string &quality, const std::string &series_directory) { + for (auto& ep : episodes) { + ep.download(quality, series_directory + "/" + this->name); + } + } +} // dropout_dl \ No newline at end of file diff --git a/src/season.h b/src/season.h new file mode 100644 index 0000000..0bf93d0 --- /dev/null +++ b/src/season.h @@ -0,0 +1,33 @@ +// +// Created by moss on 9/29/22. +// +#pragma once + +#include +#include + +#include "episode.h" + +namespace dropout_dl { + + class season { + public: + std::string name; + std::string url; + std::string page_data; + std::vector episodes; + + static std::vector get_episodes(const std::string& html_data, const std::vector& cookies); + + void download(const std::string& quality, const std::string& series_directory); + + season(const std::string& url, const std::string& name, const std::vector& cookies) { + this->url = url; + this->name = name; + this->page_data = get_generic_page(url); + this->episodes = get_episodes(page_data, cookies); + } + }; + +} // dropout_dl + diff --git a/src/series.cpp b/src/series.cpp new file mode 100644 index 0000000..7f93731 --- /dev/null +++ b/src/series.cpp @@ -0,0 +1,179 @@ +// +// Created by moss on 9/29/22. +// + +#include "series.h" + +namespace dropout_dl { + + std::string series::get_series_page(const std::string &url, bool verbose) { + CURLcode ret; + CURL *hnd; + struct curl_slist *slist1; + + std::string series_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, "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, &series_data); + + ret = curl_easy_perform(hnd); + + curl_easy_cleanup(hnd); + hnd = NULL; + curl_slist_free_all(slist1); + slist1 = NULL; + + return series_data; + } + + std::string series::get_series_name(const std::string& html_data) { + std::string collection_title("collection-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, collection_title)) { + for (int j = i + collection_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, close_tag)) { + for (int l = 0; l < html_data.size() - j; l++) { + char c = html_data[j + l]; + if (series_name_start == -1) { + if (html_data[j + l + 1] == '\n' || html_data[j + l + 1] == ' ' || + html_data[j + l + 1] == '\t') { + continue; + } else { + series_name_start = j + l + 1; + } + } + if (substr_is(html_data, j + l, close_a) || (series_name_start != -1 && html_data[j + l] == '\n')) { + return html_data.substr(series_name_start, l - (series_name_start - j)); + } + } + } + } + } + } + return "-1"; + } + + std::vector series::get_seasons(const std::string &html_data, const std::vector& cookies) { + std::vector out; + + std::string search_class("js-switch-season"); + std::string open_select(""); + std::string close_select(""); + + std::string open_option(""); + std::string value("value="); + + bool seasons_dropdown = false; + std::string season_url; + std::string season_name; + for (int i = 0; i < html_data.size(); i++) { + if (substr_is(html_data, i, open_select)) { + for (int j = i; j < html_data.size(); j++) { + if (substr_is(html_data, j, search_class)) { + i = j; + seasons_dropdown = true; + break; + } + else if (substr_is(html_data, j, close_tag)) { + break; + } + } + } + if (seasons_dropdown) { + if (substr_is(html_data, i, value)) { + i += value.size() + 1; + for (int j = 0; j + i < html_data.size(); j++) { + if (html_data[i + j] == '"') { + season_url = html_data.substr(i, j); + i += j; + break; + } + } + } + else if (!season_url.empty() && substr_is(html_data, i, close_tag)) { + i += close_tag.size() + 1; + for (int j = 0; i + j < html_data.size(); j++) { + if (html_data[i + j] == '\n') { + season_name = html_data.substr(i, j); + + // Remove leading and trailing whitespace + bool leading_whitespace = true; + int name_start; + int name_end; + for (int k = 0; k < season_name.size(); k++) { + if (season_name[k] != ' ' && season_name[k] != '\t' && season_name[k] != '\n') { + name_start = k; + break; + } + } + for (int k = season_name.size() - 1; k > 0; k--) { + if (season_name[k] != ' ' && season_name[k] != '\t' && season_name[k] != '\n') { + name_end = k; + break; + } + } + season_name = season_name.substr(name_start, season_name.size() - name_start - name_end); + + out.emplace_back(season_url, season_name, cookies); + + std::cout << out.back().name << ": " << out.back().url << '\n'; + + season_url.clear(); + season_name.clear(); + + i = i + j; + + break; + } + } + } + + if (substr_is(html_data, i, close_select)) { + break; + } + } + } + + return out; + } + + void series::download(const std::string &quality, const std::string &series_directory) { + for (auto& season : seasons) { + season.download(quality, series_directory); + } + } +} // dropout_dl \ No newline at end of file diff --git a/src/series.h b/src/series.h new file mode 100644 index 0000000..77e45a9 --- /dev/null +++ b/src/series.h @@ -0,0 +1,36 @@ +// +// Created by moss on 9/29/22. +// +#pragma once + +#include +#include + +#include "season.h" + +namespace dropout_dl { + + class series { + public: + std::string name; + std::string url; + std::string page_data; + std::vector seasons; + + static std::string get_series_page(const std::string& url, bool verbose = false); + + static std::string get_series_name(const std::string& html_data); + + static std::vector get_seasons(const std::string& html_data, const std::vector& cookies); + + void download(const std::string& quality, const std::string& series_directory); + + explicit series(const std::string& url, std::vector cookies) { + this->url = url; + this->page_data = get_series_page(url); + this->name = get_series_name(page_data); + this->seasons = get_seasons(page_data, cookies); + } + }; + +} // dropout_dl