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:
parent
f62a2f0c59
commit
1138e0fc3f
|
@ -3,7 +3,7 @@ project(dropout-dl)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
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")
|
IF (EXISTS "firefox_profile")
|
||||||
target_link_libraries(dropout-dl curl sqlite3)
|
target_link_libraries(dropout-dl curl sqlite3)
|
||||||
|
|
60
readme.md
60
readme.md
|
@ -9,47 +9,45 @@ cd <build-dir>
|
||||||
make
|
make
|
||||||
```
|
```
|
||||||
|
|
||||||
## setup
|
### Dependency Installation
|
||||||
the sqlite and curl libraries are required \
|
sqlite-devel is optional but highly recommended.
|
||||||
additionally [cookies](#cookies) are needed
|
|
||||||
|
|
||||||
### dependency installation
|
#### Void
|
||||||
#### void linux
|
|
||||||
```
|
```
|
||||||
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
|
## cookies
|
||||||
### firefox
|
### Firefox
|
||||||
#### option 1 (requires sqlite-devel)
|
#### 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)
|
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)
|
#### 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)
|
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='__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
|
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)
|
This needs to be redone every time the cookies expire (~30 minutes)
|
||||||
#### option 3
|
#### Option 3
|
||||||
open firefox and go to any dropout.tv episode \
|
Open firefox and go to any dropout.tv episode \
|
||||||
open the dev tools and go to network then refresh \
|
Open the dev tools and go to network then refresh \
|
||||||
search for `?api` and select the top request \
|
Search for `?api` and select the top request \
|
||||||
copy the `__cf_bm` cookie from the cookies section \
|
Copy the `__cf_bm` cookie from the cookies section \
|
||||||
create a file called `auth_cookie` and paste the cookie in the file \
|
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` \
|
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 everytime the cookie expires (~30 minutes)
|
||||||
### chrome
|
### chrome
|
||||||
#### option 1 (requires sqlite-devel and libgcrypt) NOT CURRENTLY FUNCTIONAL
|
#### 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))
|
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
|
#### Option 2
|
||||||
go to settings > privacy and security > cookies > see all cookies > vhx.tv > __cf_bm \
|
Go to settings > privacy and security > cookies > see all cookies > vhx.tv > __cf_bm \
|
||||||
copy the `content` and paste it into the `cookie` file \
|
Copy the `content` and paste it into the `cookie` file \
|
||||||
this needs to be redone every time the cookies expire (~30 minutes)
|
This needs to be redone every time the cookies expire (~30 minutes)
|
||||||
|
|
||||||
## How to Use
|
## 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
|
--output-directory Set the directory where files are output
|
||||||
--verbose Display debug information while running
|
--verbose Display debug information while running
|
||||||
--force-cookies Interpret the next to arguments as authentication cookie and session cookie
|
--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
|
By default, dropout-dl will download the episode in the format `<series>/S<season-num>E<episode-num><name>.mp4`
|
||||||
`S<season-num>E<episode-num><name>.mp4`
|
|
|
@ -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__)
|
#if defined(__WIN32__)
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
msec_t time_ms(void)
|
msec_t time_ms(void)
|
||||||
|
@ -82,6 +73,12 @@ namespace dropout_dl {
|
||||||
return 0;
|
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
|
// episode statics
|
||||||
std::string episode::get_series_name(const std::string& html_data) {
|
std::string episode::get_series_name(const std::string& html_data) {
|
||||||
std::string series_title("series-title");
|
std::string series_title("series-title");
|
||||||
|
@ -358,14 +355,13 @@ namespace dropout_dl {
|
||||||
return embedded_page;
|
return embedded_page;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string episode::get_config_page(const std::string& url, bool verbose) {
|
std::string get_generic_page(const std::string& url, bool verbose) {
|
||||||
CURLcode ret;
|
|
||||||
CURL *hnd;
|
CURL *hnd;
|
||||||
struct curl_slist *slist1;
|
struct curl_slist *slist1;
|
||||||
|
|
||||||
std::string config_page;
|
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, "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: */*");
|
||||||
slist1 = curl_slist_append(slist1, "Accept-Language: en-US,en;q=0.5");
|
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_WRITEFUNCTION, WriteCallback);
|
||||||
curl_easy_setopt(hnd, CURLOPT_WRITEDATA, &config_page);
|
curl_easy_setopt(hnd, CURLOPT_WRITEDATA, &config_page);
|
||||||
|
|
||||||
/* Here is a list of options the curl code used that cannot get generated
|
curl_easy_perform(hnd);
|
||||||
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);
|
curl_easy_cleanup(hnd);
|
||||||
hnd = NULL;
|
hnd = nullptr;
|
||||||
curl_slist_free_all(slist1);
|
curl_slist_free_all(slist1);
|
||||||
slist1 = NULL;
|
slist1 = nullptr;
|
||||||
|
|
||||||
return config_page;
|
return config_page;
|
||||||
}
|
}
|
||||||
|
@ -468,7 +446,6 @@ namespace dropout_dl {
|
||||||
return qualities;
|
return qualities;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::string episode::get_video_url(const std::string& quality) {
|
std::string episode::get_video_url(const std::string& quality) {
|
||||||
for (int i = 0; i < qualities.size(); i++) {
|
for (int i = 0; i < qualities.size(); i++) {
|
||||||
if (qualities[i] == quality) {
|
if (qualities[i] == quality) {
|
||||||
|
@ -492,7 +469,7 @@ namespace dropout_dl {
|
||||||
std::string out;
|
std::string out;
|
||||||
|
|
||||||
curl_easy_setopt(curl, CURLOPT_URL, get_video_url(quality).c_str());
|
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_WRITEDATA, &out);
|
||||||
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false);
|
curl_easy_setopt(curl, CURLOPT_NOPROGRESS, false);
|
||||||
curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, dropout_dl::curl_progress_func);
|
curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION, dropout_dl::curl_progress_func);
|
||||||
|
@ -503,4 +480,33 @@ namespace dropout_dl {
|
||||||
}
|
}
|
||||||
return "CURL ERROR";
|
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
|
} // dropout_dl
|
|
@ -7,6 +7,8 @@
|
||||||
#include <curl/curl.h>
|
#include <curl/curl.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
|
#include <fstream>
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
namespace dropout_dl {
|
namespace dropout_dl {
|
||||||
|
|
||||||
|
@ -14,8 +16,6 @@ namespace dropout_dl {
|
||||||
|
|
||||||
void replace_all(std::string& str, const std::string& from, const std::string& to);
|
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__)
|
#if defined(__WIN32__)
|
||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
msec_t time_ms(void);
|
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);
|
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 {
|
class episode {
|
||||||
|
|
||||||
public:
|
public:
|
||||||
|
@ -42,22 +46,14 @@ namespace dropout_dl {
|
||||||
std::vector<std::string> qualities;
|
std::vector<std::string> qualities;
|
||||||
std::vector<std::string> quality_urls;
|
std::vector<std::string> quality_urls;
|
||||||
|
|
||||||
bool verbose;
|
bool verbose = false;
|
||||||
|
|
||||||
// Curl
|
// 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_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_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
|
// Parsing
|
||||||
static std::string get_series_name(const std::string& html_data);
|
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);
|
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->episode_url = episode_url;
|
||||||
this->verbose = verbose;
|
this->verbose = verbose;
|
||||||
|
|
||||||
|
@ -141,10 +140,12 @@ namespace dropout_dl {
|
||||||
std::cout << "Got config url: " << this->embedded_url << '\n';
|
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();
|
this->get_qualities();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
episode() = default;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // dropout_dl
|
} // dropout_dl
|
||||||
|
|
188
src/main.cpp
188
src/main.cpp
|
@ -1,8 +1,6 @@
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <fstream>
|
|
||||||
#include <filesystem>
|
|
||||||
|
|
||||||
#include "episode.h"
|
#include "series.h"
|
||||||
|
|
||||||
#ifdef DROPOUT_DL_SQLITE
|
#ifdef DROPOUT_DL_SQLITE
|
||||||
#include <sqlite3.h>
|
#include <sqlite3.h>
|
||||||
|
@ -320,9 +318,12 @@ public:
|
||||||
std::string url;
|
std::string url;
|
||||||
bool verbose = false;
|
bool verbose = false;
|
||||||
bool cookies_forced = false;
|
bool cookies_forced = false;
|
||||||
|
bool series = false;
|
||||||
std::string quality;
|
std::string quality;
|
||||||
std::string filename;
|
std::string filename;
|
||||||
std::string series_dir;
|
std::string series_dir;
|
||||||
|
std::string episode;
|
||||||
|
std::string season;
|
||||||
std::vector<std::string> cookies;
|
std::vector<std::string> cookies;
|
||||||
|
|
||||||
static std::vector<std::string> convert_program_args(int argc, char** argv) {
|
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);
|
std::vector<std::string> args = convert_program_args(argc, argv);
|
||||||
|
|
||||||
for (int i = 0; i < args.size(); i++) {
|
for (int i = 0; i < args.size(); i++) {
|
||||||
const auto& arg = args[i];
|
std::string arg = args[i];
|
||||||
|
|
||||||
if (arg.substr(0, 2) != "--") {
|
if (arg.substr(0, 2) != "--") {
|
||||||
url = arg;
|
url = arg;
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
else {
|
arg = arg.substr(2);
|
||||||
if (arg == "--verbose") {
|
if (arg == "verbose") {
|
||||||
verbose = true;
|
verbose = true;
|
||||||
} else if (arg == "--quality") {
|
} else if (arg == "quality") {
|
||||||
if (i + 1 >= args.size()) {
|
if (i + 1 >= args.size()) {
|
||||||
std::cerr << "ARGUMENT PARSE ERROR: --quality used with too few following arguments\n";
|
std::cerr << "ARGUMENT PARSE ERROR: --quality used with too few following arguments\n";
|
||||||
exit(8);
|
exit(8);
|
||||||
}
|
|
||||||
quality = args[++i];
|
|
||||||
}
|
}
|
||||||
else if (arg == "--force-cookies") {
|
quality = args[++i];
|
||||||
if (i + 2 >= args.size()) {
|
}
|
||||||
std::cerr << "ARGUMENT PARSE ERROR: --force-cookies used with too few following arguments\n";
|
else if (arg == "force-cookies") {
|
||||||
exit(8);
|
if (i + 2 >= args.size()) {
|
||||||
}
|
std::cerr << "ARGUMENT PARSE ERROR: --force-cookies used with too few following arguments\n";
|
||||||
cookies.emplace_back(args[++i]);
|
exit(8);
|
||||||
cookies.emplace_back(args[++i]);
|
|
||||||
cookies_forced = true;
|
|
||||||
}
|
}
|
||||||
else if (arg == "--output") {
|
cookies.emplace_back(args[++i]);
|
||||||
if (i + 1 >= args.size()) {
|
cookies.emplace_back(args[++i]);
|
||||||
std::cerr << "ARGUMENT PARSE ERROR: --output used with too few following arguments\n";
|
cookies_forced = true;
|
||||||
exit(8);
|
}
|
||||||
}
|
else if (arg == "output") {
|
||||||
filename = args[++i];
|
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") {
|
filename = args[++i];
|
||||||
if (i + 1 >= args.size()) {
|
}
|
||||||
std::cerr << "ARGUMENT PARSE ERROR: --output-directory used with too few following arguments\n";
|
else if (arg == "output-directory") {
|
||||||
exit(8);
|
if (i + 1 >= args.size()) {
|
||||||
}
|
std::cerr << "ARGUMENT PARSE ERROR: --output-directory used with too few following arguments\n";
|
||||||
series_dir = args[++i];
|
exit(8);
|
||||||
}
|
}
|
||||||
else if (arg == "--help") {
|
series_dir = args[++i];
|
||||||
std::cout << "Usage: dropout-dl [OPTIONS] <url> [OPTIONS]\n"
|
}
|
||||||
"\n"
|
else if (arg == "series") {
|
||||||
"Options:\n"
|
series = true;
|
||||||
"\t--help Display this message\n"
|
}
|
||||||
"\t--quality Set the quality of the downloaded video. Quality can be set to 'all' which\n"
|
else if (arg == "episode") {
|
||||||
"\t will download all qualities and place them into separate folders\n"
|
if (i + 1 >= args.size()) {
|
||||||
"\t--output Set the output filename\n"
|
std::cerr << "ARGUMENT PARSE ERROR: --episode used with too few following arguments\n";
|
||||||
"\t--output-directory Set the directory where files are output\n"
|
exit(8);
|
||||||
"\t--verbose Display debug information while running\n"
|
}
|
||||||
"\t--force-cookies Interpret the next to arguments as authentication cookie and session cookie\n"
|
episode = args[++i];
|
||||||
<< std::endl;
|
}
|
||||||
|
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) {
|
int main(int argc, char** argv) {
|
||||||
|
|
||||||
options options(argc, argv);
|
options options(argc, argv);
|
||||||
|
|
||||||
std::cout << "quality: " << options.quality << std::endl;
|
std::cout << "quality: " << options.quality << std::endl;
|
||||||
|
@ -425,52 +445,50 @@ int main(int argc, char** argv) {
|
||||||
options.cookies = get_cookies(options.verbose);
|
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()) {
|
if (options.series_dir.empty()) {
|
||||||
options.filename = "S" + (ep.season_number.size() < 2 ? "0" + ep.season_number : ep.season_number) + "E" +
|
options.series_dir = series.name;
|
||||||
(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.series_dir.begin(), options.series_dir.end(), ' ', '_');
|
||||||
|
|
||||||
std::replace(options.filename.begin(), options.filename.end(), ',', '_');
|
std::replace(options.series_dir.begin(), options.series_dir.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';
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (options.quality == "all") {
|
series.download(options.quality, options.series_dir);
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else {
|
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;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
@ -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
|
Loading…
Reference in New Issue