Options: Added '--series' Option

This allows for an entire series to be downloaded at once. There are
issues with filenames but it works mostly.
This commit is contained in:
Moss 2022-09-29 03:04:57 -04:00
parent f62a2f0c59
commit 1138e0fc3f
9 changed files with 497 additions and 164 deletions

View File

@ -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)

View File

@ -9,47 +9,45 @@ cd <build-dir>
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';" > <build-dir>/auth_cookie
sqlite3 cookies.sqlite "SELECT value FROM moz_cookies WHERE host LIKE '%dropout.tv%' AND name='_session';" > <build-dir>/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
`S<season-num>E<episode-num><name>.mp4`
By default, dropout-dl will download the episode in the format `<series>/S<season-num>E<episode-num><name>.mp4`

View File

@ -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 <windows.h>
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

View File

@ -7,6 +7,8 @@
#include <curl/curl.h>
#include <vector>
#include <algorithm>
#include <fstream>
#include <filesystem>
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 <windows.h>
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<std::string> qualities;
std::vector<std::string> 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<std::string> cookies, bool verbose = false) {
std::cout << episode_url << std::endl;
explicit episode(const std::string& episode_url, std::vector<std::string> 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

View File

@ -1,8 +1,6 @@
#include <iostream>
#include <fstream>
#include <filesystem>
#include "episode.h"
#include "series.h"
#ifdef DROPOUT_DL_SQLITE
#include <sqlite3.h>
@ -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<std::string> cookies;
static std::vector<std::string> convert_program_args(int argc, char** argv) {
@ -337,59 +338,79 @@ public:
std::vector<std::string> 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] <url> [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] <url> [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;
}

60
src/season.cpp Normal file
View File

@ -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<std::string>& cookies) {
int link_start = 0;
for (int i = start_point; i > 0; i--) {
if (substr_is(html_data, i, "<a")) {
link_start = i;
break;
}
else if (substr_is(html_data, i, "<")) {
// Invalid episode place. Return empty value.
return {};
}
}
for (int i = link_start; i < html_data.size(); i++) {
if (substr_is(html_data, i, "href=\"")) {
i += 6;
for (int j = 0; j + i < html_data.size(); j++) {
if (html_data[i + j] == '"') {
start_point += 15;
return episode(html_data.substr(i, j), cookies);
}
}
}
}
std::cerr << "SEASON PARSE ERROR: Error finding episode" << std::endl;
exit(8);
}
std::vector<episode> season::get_episodes(const std::string &html_data, const std::vector<std::string>& cookies) {
std::vector<episode> 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

33
src/season.h Normal file
View File

@ -0,0 +1,33 @@
//
// Created by moss on 9/29/22.
//
#pragma once
#include <iostream>
#include <vector>
#include "episode.h"
namespace dropout_dl {
class season {
public:
std::string name;
std::string url;
std::string page_data;
std::vector<episode> episodes;
static std::vector<episode> get_episodes(const std::string& html_data, const std::vector<std::string>& cookies);
void download(const std::string& quality, const std::string& series_directory);
season(const std::string& url, const std::string& name, const std::vector<std::string>& cookies) {
this->url = url;
this->name = name;
this->page_data = get_generic_page(url);
this->episodes = get_episodes(page_data, cookies);
}
};
} // dropout_dl

179
src/series.cpp Normal file
View File

@ -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("<h1");
std::string close_tag(">");
std::string close_a("</h1>");
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<season> series::get_seasons(const std::string &html_data, const std::vector<std::string>& cookies) {
std::vector<season> out;
std::string search_class("js-switch-season");
std::string open_select("<select");
std::string close_tag(">");
std::string close_select("</select>");
std::string open_option("<option");
std::string close_option("</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

36
src/series.h Normal file
View File

@ -0,0 +1,36 @@
//
// Created by moss on 9/29/22.
//
#pragma once
#include <iostream>
#include <vector>
#include "season.h"
namespace dropout_dl {
class series {
public:
std::string name;
std::string url;
std::string page_data;
std::vector<season> 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<season> get_seasons(const std::string& html_data, const std::vector<std::string>& cookies);
void download(const std::string& quality, const std::string& series_directory);
explicit series(const std::string& url, std::vector<std::string> 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