Initial commit
This commit is contained in:
commit
00071051d3
16
.gitignore
vendored
Normal file
16
.gitignore
vendored
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
.dub
|
||||||
|
docs.json
|
||||||
|
__dummy.html
|
||||||
|
docs/
|
||||||
|
/hentai_downloader
|
||||||
|
hentai_downloader.so
|
||||||
|
hentai_downloader.dylib
|
||||||
|
hentai_downloader.dll
|
||||||
|
hentai_downloader.a
|
||||||
|
hentai_downloader.lib
|
||||||
|
hentai_downloader-test-*
|
||||||
|
*.exe
|
||||||
|
*.o
|
||||||
|
*.obj
|
||||||
|
*.lst
|
||||||
|
list.txt
|
24
README.md
Normal file
24
README.md
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
# Hentai Downloader
|
||||||
|
|
||||||
|
## Prerequisites
|
||||||
|
* dub
|
||||||
|
* dmd or ldc2
|
||||||
|
|
||||||
|
## Building hentai_downloader
|
||||||
|
|
||||||
|
`dub build`
|
||||||
|
|
||||||
|
# Config file
|
||||||
|
|
||||||
|
On the first run of the program a config.json files is created in
|
||||||
|
`~/.config/hentai_downloader`
|
||||||
|
|
||||||
|
## Sites:
|
||||||
|
* [X] hentai.cafe
|
||||||
|
* [X] nhentai.net
|
||||||
|
* [ ] all the other ones
|
||||||
|
|
||||||
|
## TODO:
|
||||||
|
* [ ] Create a makefile that builds and installs the hentai_downloader into /usr/local/bin
|
||||||
|
* [ ] Fix the FIXMEs in the code
|
||||||
|
* [ ] Optional flag to compress the downloaded folders
|
9
dub.json
Normal file
9
dub.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"authors": [
|
||||||
|
""
|
||||||
|
],
|
||||||
|
"copyright": "Copyright © 2020, ",
|
||||||
|
"description": "Download doujins/hentai from the commandline",
|
||||||
|
"license": "proprietary",
|
||||||
|
"name": "hentai_downloader"
|
||||||
|
}
|
63
source/app.d
Normal file
63
source/app.d
Normal file
@ -0,0 +1,63 @@
|
|||||||
|
import std.stdio;
|
||||||
|
import std.string;
|
||||||
|
|
||||||
|
// My classes
|
||||||
|
import config.downloaderconfig;
|
||||||
|
|
||||||
|
import sites.hentaicafe;
|
||||||
|
import sites.nhentai;
|
||||||
|
|
||||||
|
import inputhandler;
|
||||||
|
|
||||||
|
void printHelp()
|
||||||
|
{
|
||||||
|
writeln(`
|
||||||
|
Usage:
|
||||||
|
-h Display this help message
|
||||||
|
-b <text file> Batchmode -> Downloads all links in the given text file
|
||||||
|
<link> Download only one manga`);
|
||||||
|
}
|
||||||
|
|
||||||
|
void main(string[] args)
|
||||||
|
{
|
||||||
|
/* writeln(args); */
|
||||||
|
|
||||||
|
if(args.length < 2)
|
||||||
|
{
|
||||||
|
printHelp();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(args.length == 2)
|
||||||
|
{
|
||||||
|
// Direct link was supplied
|
||||||
|
string url = args[1];
|
||||||
|
|
||||||
|
// Call the factory with the link that was supplied
|
||||||
|
siteFactory(url);
|
||||||
|
}
|
||||||
|
else if(args.length >= 3)
|
||||||
|
{
|
||||||
|
// Batchmode
|
||||||
|
import std.file : readText;
|
||||||
|
|
||||||
|
string filename = args[2];
|
||||||
|
// Read all the links into memory
|
||||||
|
string fileContents = readText(filename);
|
||||||
|
// Transform fileContents into an array
|
||||||
|
string[] urls = fileContents.split("\n");
|
||||||
|
|
||||||
|
// Sanitize the urls
|
||||||
|
foreach(string url; urls)
|
||||||
|
{
|
||||||
|
// If the url is empty move on to the next
|
||||||
|
if(strip(url) == "") continue;
|
||||||
|
url = strip(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* writeln(urls); */
|
||||||
|
|
||||||
|
// Call the factory
|
||||||
|
siteFactory(urls);
|
||||||
|
}
|
||||||
|
}
|
219
source/config/downloaderconfig.d
Normal file
219
source/config/downloaderconfig.d
Normal file
@ -0,0 +1,219 @@
|
|||||||
|
module config.downloaderconfig;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This struct represents the config of this
|
||||||
|
+ programm and is used by the other classes
|
||||||
|
+ to configure stuff
|
||||||
|
+/
|
||||||
|
struct Config
|
||||||
|
{
|
||||||
|
/++
|
||||||
|
+ This variable holds the folder/path into which
|
||||||
|
+ mangas are downloaded by default
|
||||||
|
+/
|
||||||
|
string standard_download_folder;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This variable determins if mangas should be downloaded
|
||||||
|
+ again even if they already are downloaded
|
||||||
|
+/
|
||||||
|
bool redownload_mangas_regardless;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This variable determins if the downloader should output debug infos
|
||||||
|
+/
|
||||||
|
bool enable_debug_output;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This class handles reading and creating the config files
|
||||||
|
+/
|
||||||
|
class DownloaderConfig
|
||||||
|
{
|
||||||
|
static private:
|
||||||
|
import std.stdio : writeln;
|
||||||
|
import std.json : JSONValue, parseJSON;
|
||||||
|
import std.file : exists, mkdir, readText, copy;
|
||||||
|
import std.stdio : toFile;
|
||||||
|
import std.string: strip;
|
||||||
|
import std.array : replace;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This specifies the default path to the config
|
||||||
|
+/
|
||||||
|
string default_config_path = "/.config/hentai_downloader/config.json";
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This specifies the default path to the template file config file
|
||||||
|
+/
|
||||||
|
immutable string default_config_template_path = "./default_config_template.json";
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This holds the text of the `default_config_template.json` file in case it
|
||||||
|
+ doesn't exist in the default path anymore
|
||||||
|
+/
|
||||||
|
immutable string default_config_template_text =
|
||||||
|
`
|
||||||
|
{
|
||||||
|
"standard_download_folder" : "~/Downloads/Lewds/",
|
||||||
|
"redownload_mangas_regardless" : false,
|
||||||
|
"enable_debug_output" : false
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function checks the config in `config_path` exists
|
||||||
|
+/
|
||||||
|
bool configExists(string config_path)
|
||||||
|
{
|
||||||
|
return exists(config_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function creates a new default config at `config_path`
|
||||||
|
+/
|
||||||
|
void createNewConfig(string config_path)
|
||||||
|
{
|
||||||
|
writeln("[*] Creating new config file in ", config_path);
|
||||||
|
|
||||||
|
// The folder that holds the config file
|
||||||
|
immutable string config_dir = "/home/" ~ getUsername() ~ "/.config/hentai_downloader";
|
||||||
|
|
||||||
|
/* writeln("Config folder:", config_dir); */
|
||||||
|
|
||||||
|
// if the folder ~/.config/hentai_downloader doesnt exist
|
||||||
|
if(!exists(config_dir))
|
||||||
|
mkdir(config_dir); // Create it
|
||||||
|
|
||||||
|
/* writeln("Config file: ", config_path); */
|
||||||
|
|
||||||
|
// Write to template file into the config folder
|
||||||
|
if(!exists(config_path))
|
||||||
|
{
|
||||||
|
/* writeln("Saving default config to file: ", config_path); */
|
||||||
|
default_config_template_text.toFile(config_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ Turns a relative path e.g. "~/.config" into an absolute path "/home/user/.config"
|
||||||
|
+/
|
||||||
|
string makeRelativePathAbsolute(string path)
|
||||||
|
{
|
||||||
|
return path.replace("~", "/home/"~getUsername());
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function checks if the folder specified in the config file exists
|
||||||
|
+ and if not creates it
|
||||||
|
+/
|
||||||
|
void checkStandardFolder(Config config)
|
||||||
|
{
|
||||||
|
// Replace the relative path
|
||||||
|
string absolutePath = makeRelativePathAbsolute(config.standard_download_folder);
|
||||||
|
|
||||||
|
if(!exists(absolutePath))
|
||||||
|
{
|
||||||
|
// If the `standard_download_folder` doesnt exist, create it!
|
||||||
|
absolutePath.mkdir();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ Gets the username of the current user
|
||||||
|
+/
|
||||||
|
string getUsername()
|
||||||
|
{
|
||||||
|
import std.process : executeShell;
|
||||||
|
import core.stdc.stdlib : exit, EXIT_FAILURE;
|
||||||
|
// Execute whoami to get the current username
|
||||||
|
auto whoami = executeShell(`whoami`);
|
||||||
|
if(whoami.status != 0)
|
||||||
|
{
|
||||||
|
// If whoami fails exit the program
|
||||||
|
// FIXME: raise an exception or something
|
||||||
|
writeln("[!] Failed to get the current username");
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
// Return the stripped username
|
||||||
|
return whoami.output.strip();
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ Get the users .config path
|
||||||
|
+/
|
||||||
|
string getUserConfigPath()
|
||||||
|
{
|
||||||
|
// Return the combined path to the .config folder of the user
|
||||||
|
return "/home/" ~ getUsername() ~ default_config_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function trys to parse the config file
|
||||||
|
+ given in `config_path`, and return a Config struct
|
||||||
|
+/
|
||||||
|
Config parseConfig(string config_path)
|
||||||
|
{
|
||||||
|
import std.conv : to;
|
||||||
|
|
||||||
|
// Read the config file
|
||||||
|
string config_text = to!string(readText(config_path));
|
||||||
|
|
||||||
|
// Parse the config as JSON
|
||||||
|
auto config_json = parseJSON(config_text);
|
||||||
|
|
||||||
|
// Create a new config
|
||||||
|
Config _config;
|
||||||
|
|
||||||
|
// Assign the values that were parsed from the config file
|
||||||
|
_config.standard_download_folder = config_json["standard_download_folder"].str();
|
||||||
|
_config.redownload_mangas_regardless = config_json["redownload_mangas_regardless"].boolean();
|
||||||
|
_config.enable_debug_output = config_json["enable_debug_output"].boolean();
|
||||||
|
|
||||||
|
// Adjust the foler path
|
||||||
|
_config.standard_download_folder = makeRelativePathAbsolute(_config.standard_download_folder);
|
||||||
|
|
||||||
|
return _config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ Loads and checks the given config
|
||||||
|
+/
|
||||||
|
Config loadMyConfig(string config_path)
|
||||||
|
{
|
||||||
|
// Get the cofig path for this user
|
||||||
|
config_path = getUserConfigPath();
|
||||||
|
|
||||||
|
// Check if the given config exists
|
||||||
|
if(!configExists(config_path))
|
||||||
|
createNewConfig(config_path); // If it doesn't, create it!
|
||||||
|
|
||||||
|
// Parse the config
|
||||||
|
Config _config = parseConfig(config_path);
|
||||||
|
|
||||||
|
// Check the download folder
|
||||||
|
checkStandardFolder(_config);
|
||||||
|
|
||||||
|
return _config;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public:
|
||||||
|
/++
|
||||||
|
+ This loads and parses the default config at `default_config_path`
|
||||||
|
+ into a `Config` struct
|
||||||
|
+/
|
||||||
|
static Config loadConfig()
|
||||||
|
{
|
||||||
|
return loadMyConfig(this.default_config_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This loads and parses a custom config from `custom_config_path`
|
||||||
|
+ into a `Config` struct
|
||||||
|
+/
|
||||||
|
static Config loadConfig(string custom_config_path)
|
||||||
|
{
|
||||||
|
return loadMyConfig(custom_config_path);
|
||||||
|
}
|
||||||
|
}
|
68
source/inputhandler.d
Normal file
68
source/inputhandler.d
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import std.stdio;
|
||||||
|
import std.string;
|
||||||
|
import std.array;
|
||||||
|
import core.stdc.stdlib : exit, EXIT_FAILURE;
|
||||||
|
|
||||||
|
import config.downloaderconfig;
|
||||||
|
|
||||||
|
import sites.basesite;
|
||||||
|
import sites.hentaicafe;
|
||||||
|
import sites.nhentai;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function parses the url and creates the appropriate site object
|
||||||
|
+ and then downloads the images
|
||||||
|
+/
|
||||||
|
void siteFactory(string url)
|
||||||
|
{
|
||||||
|
immutable string hentaicafe_indicator = "/hc.fyi/";
|
||||||
|
immutable string nhentai_indicator = "/g/";
|
||||||
|
|
||||||
|
// Load the config file
|
||||||
|
Config config = DownloaderConfig.loadConfig();
|
||||||
|
|
||||||
|
// Placeholder for down casted object
|
||||||
|
BaseSite mangaSite;
|
||||||
|
|
||||||
|
if(indexOf(url, hentaicafe_indicator) != -1) // The supplied url is a hentaicafe url
|
||||||
|
{
|
||||||
|
// Create `HentaiCafe` object
|
||||||
|
HentaiCafe hentaicafe = new HentaiCafe(config);
|
||||||
|
|
||||||
|
// Implicit downcast to `BaseSite`
|
||||||
|
mangaSite = hentaicafe;
|
||||||
|
}
|
||||||
|
else if(indexOf(url, nhentai_indicator) != -1) // The supplied url is a nhentai url
|
||||||
|
{
|
||||||
|
// Create `NHentai` object
|
||||||
|
NHentai nhentai = new NHentai(config);
|
||||||
|
|
||||||
|
// Implicit downcast to `BaseSite`
|
||||||
|
mangaSite = nhentai;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
writeln("[!] The url you supplied isn't supported :(");
|
||||||
|
|
||||||
|
writeln(url);
|
||||||
|
// FIXME:
|
||||||
|
// Dont exit with a failure
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the manga
|
||||||
|
mangaSite.downloadDoujin(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function parses each url in a list
|
||||||
|
+ and creates the appropriate site object to
|
||||||
|
+ download the managa
|
||||||
|
+/
|
||||||
|
void siteFactory(string[] urls)
|
||||||
|
{
|
||||||
|
// Call the site factory for each url that way you can have a list
|
||||||
|
// of mixed manag links
|
||||||
|
foreach(string url; urls)
|
||||||
|
siteFactory(url);
|
||||||
|
}
|
166
source/sites/basesite.d
Normal file
166
source/sites/basesite.d
Normal file
@ -0,0 +1,166 @@
|
|||||||
|
module sites.basesite;
|
||||||
|
|
||||||
|
import config.downloaderconfig;
|
||||||
|
import sites.basesiteintf;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This is the baseclass which all
|
||||||
|
+ other site classes inherit from
|
||||||
|
+/
|
||||||
|
class BaseSite : BaseSiteIntf
|
||||||
|
{
|
||||||
|
private:
|
||||||
|
Config _config;
|
||||||
|
|
||||||
|
protected:
|
||||||
|
import std.stdio : writeln, writefln;
|
||||||
|
import std.file : exists, rmdirRecurse, mkdir;
|
||||||
|
import std.json : parseJSON, JSONValue;
|
||||||
|
import std.array : replace, split;
|
||||||
|
import std.string : indexOf;
|
||||||
|
import std.net.curl : download;
|
||||||
|
|
||||||
|
// This function needs to be implemented by each derived site class
|
||||||
|
abstract string getNameFromUrl(string url);
|
||||||
|
|
||||||
|
// This function needs to be implemented by each derived site class
|
||||||
|
abstract string[] getImageUrlsFromBase(string url);
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ Gets the image urls from the supplied json
|
||||||
|
+ each derived site class should override this class
|
||||||
|
+ if the image urls aren't stored in the way this method
|
||||||
|
+ expects them to be
|
||||||
|
+/
|
||||||
|
string[] getUrlsFromJson(string json)
|
||||||
|
{
|
||||||
|
string[] urls;
|
||||||
|
// Parse the json
|
||||||
|
JSONValue parsedJson = parseJSON(json);
|
||||||
|
|
||||||
|
// Extract the urls of the images
|
||||||
|
foreach(JSONValue val; parsedJson.array)
|
||||||
|
urls ~= val["url"].str.replace("\\", "");
|
||||||
|
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function creates a folder with the supplied name.
|
||||||
|
+ If the folder already exists the folder will get deleted!!
|
||||||
|
+/
|
||||||
|
void createOuputFolder(string foldername)
|
||||||
|
{
|
||||||
|
// Check if foler exits already
|
||||||
|
if(exists(foldername))
|
||||||
|
{
|
||||||
|
writefln(`[!] Folder with the name "%s" exists already...`, foldername);
|
||||||
|
writeln("[!] Deleting it now!");
|
||||||
|
rmdirRecurse(foldername);
|
||||||
|
}
|
||||||
|
|
||||||
|
writefln(`[*] Creating folder "%s"`, foldername);
|
||||||
|
mkdir(foldername);
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function extracts the name of a file from the supplied url
|
||||||
|
+/
|
||||||
|
string extractFileNameFromUrl(string url)
|
||||||
|
{
|
||||||
|
string[] tmpString = url.split("/");
|
||||||
|
return tmpString[tmpString.length-1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function downloads the images over the
|
||||||
|
+ url supplied in the `imageUrls` into the `outputPath`
|
||||||
|
+/
|
||||||
|
void downloadImages(string[] imageUrls, string outputPath)
|
||||||
|
{
|
||||||
|
foreach(string url; imageUrls)
|
||||||
|
{
|
||||||
|
// Extract the filename from the url
|
||||||
|
string filepath = outputPath ~ extractFileNameFromUrl(url);
|
||||||
|
|
||||||
|
if(_config.enable_debug_output) writefln("[i] Downloading from %s ==> %s", url, filepath);
|
||||||
|
|
||||||
|
// Download the image
|
||||||
|
download(url, filepath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ Downloads a doujin from `url` into the `outputPath`
|
||||||
|
+/
|
||||||
|
void downloadDoujinFromUrl(string url, string outputPath)
|
||||||
|
{
|
||||||
|
// Create a folder with the name of the managa
|
||||||
|
createOuputFolder(outputPath);
|
||||||
|
|
||||||
|
// Extract the urls of the managa images
|
||||||
|
string[] urls = getImageUrlsFromBase(url);
|
||||||
|
|
||||||
|
// Download the images over the extracted urls
|
||||||
|
downloadImages(urls, outputPath);
|
||||||
|
|
||||||
|
writeln("[*] Done downloading...");
|
||||||
|
}
|
||||||
|
|
||||||
|
public:
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This constructor is to setup the site class with the
|
||||||
|
+ supplied `Config`
|
||||||
|
+/
|
||||||
|
this(Config config)
|
||||||
|
{
|
||||||
|
// Set the config
|
||||||
|
_config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function downloads a doujin from the supplied url
|
||||||
|
+/
|
||||||
|
void downloadDoujin(string url)
|
||||||
|
{
|
||||||
|
// Get the name of the doujin
|
||||||
|
string _foldername = _config.standard_download_folder ~ getNameFromUrl(url) ~ "/";
|
||||||
|
|
||||||
|
// Check if the folder already exists and `redownload_mangas_regardless` is set to false
|
||||||
|
if(exists(_foldername) && !_config.redownload_mangas_regardless)
|
||||||
|
{
|
||||||
|
// Then stop downloading
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if(_config.enable_debug_output) writefln("[i] _foldername is ----> %s", _foldername);
|
||||||
|
|
||||||
|
// Download the doujin into a folder with the name of the doujin
|
||||||
|
downloadDoujinFromUrl(url, _foldername);
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function downloads multiple doujins
|
||||||
|
+/
|
||||||
|
void downloadDoujin(string[] urls)
|
||||||
|
{
|
||||||
|
foreach(string url; urls)
|
||||||
|
{
|
||||||
|
// Get the name of the doujin
|
||||||
|
string _foldername = _config.standard_download_folder ~ getNameFromUrl(url) ~ "/";
|
||||||
|
|
||||||
|
if(_config.enable_debug_output) writefln("[i] _foldername is :s%", _foldername);
|
||||||
|
|
||||||
|
// Check if the folder already exists and `redownload_mangas_regardless` is set to false
|
||||||
|
if(exists(_foldername) && !_config.redownload_mangas_regardless)
|
||||||
|
{
|
||||||
|
// Then continue to the next url in the list
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Download the doujin into a folder with the name of the doujin
|
||||||
|
downloadDoujinFromUrl(url, _foldername);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
54
source/sites/basesiteintf.d
Normal file
54
source/sites/basesiteintf.d
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
module sites.basesiteintf;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This is the interface for the base class
|
||||||
|
+ from which all the other sites are inherited
|
||||||
|
+/
|
||||||
|
interface BaseSiteIntf
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
/++
|
||||||
|
+ This function returns the name of the manga by parsing the
|
||||||
|
+ html from the url
|
||||||
|
+/
|
||||||
|
string getNameFromUrl(string url);
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function parses the json in `json`
|
||||||
|
+ and returns a string array containing all
|
||||||
|
+ the urls extracted from the `json` arg
|
||||||
|
+/
|
||||||
|
string[] getUrlsFromJson(string json);
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function extracts the urls of the images from the supplied manga base url
|
||||||
|
+/
|
||||||
|
string[] getImageUrlsFromBase(string url);
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function creates a folder with the given name
|
||||||
|
+/
|
||||||
|
void createOuputFolder(string foldername);
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function downloads the images over the
|
||||||
|
+ url supplied in the `imageUrls` into the `outputPath`
|
||||||
|
+/
|
||||||
|
void downloadImages(string[] imageUrls, string outputPath);
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ Downloads a doujin from `url` into `outputPath`
|
||||||
|
+/
|
||||||
|
void downloadDoujinFromUrl(string url, string outputPath);
|
||||||
|
|
||||||
|
public:
|
||||||
|
/++
|
||||||
|
+ Downloads a dojin from `url` into the `outputPath`
|
||||||
|
+/
|
||||||
|
void downloadDoujin(string url);
|
||||||
|
|
||||||
|
/* /++
|
||||||
|
+ Download multiple doujins
|
||||||
|
+/
|
||||||
|
void downloadDojin(string[] urls); */
|
||||||
|
}
|
80
source/sites/hentaicafe.d
Normal file
80
source/sites/hentaicafe.d
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
module sites.hentaicafe;
|
||||||
|
|
||||||
|
import config.downloaderconfig;
|
||||||
|
import sites.basesite;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This class handles downloads for the site `hentai.cafe`
|
||||||
|
+/
|
||||||
|
class HentaiCafe : BaseSite
|
||||||
|
{
|
||||||
|
protected:
|
||||||
|
import std.net.curl : get;
|
||||||
|
import std.conv : to;
|
||||||
|
import std.regex : regex, match;
|
||||||
|
import core.stdc.stdlib : exit, EXIT_FAILURE;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function gets the name of the the manga by the url
|
||||||
|
+/
|
||||||
|
override string getNameFromUrl(string url)
|
||||||
|
{
|
||||||
|
// Get the site html as a string
|
||||||
|
string siteContent = to!string(get(url));
|
||||||
|
|
||||||
|
// Find the name of the manga
|
||||||
|
auto nameRegex = `<h3>(.*)</h3>`.regex;
|
||||||
|
auto nameMatch = match(siteContent, nameRegex);
|
||||||
|
|
||||||
|
// Return only the name not the html tags
|
||||||
|
return nameMatch.captures[1];
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+
|
||||||
|
+/
|
||||||
|
override string[] getImageUrlsFromBase(string url)
|
||||||
|
{
|
||||||
|
// Check if the url is a hentai.cafe comic url
|
||||||
|
if(indexOf(url, "/hc.fyi/") == -1)
|
||||||
|
{
|
||||||
|
writefln(`[!] The given url doesn't contain "/hc.fyi/" it was ignored!`);
|
||||||
|
// FIXME: no! :<
|
||||||
|
exit(EXIT_FAILURE);
|
||||||
|
}
|
||||||
|
|
||||||
|
// regex patterns for finding urls
|
||||||
|
auto comicRegex = `\"(https://hentai.cafe/manga/read/.*)\" title`.regex;
|
||||||
|
auto jsonInfoRegex = `var pages = \[(.*)\]`.regex;
|
||||||
|
|
||||||
|
// Get page html
|
||||||
|
string comicHTML = to!string(get(url));
|
||||||
|
|
||||||
|
// Find the url in the html mess
|
||||||
|
auto comicUrlMatch = match(comicHTML, comicRegex);
|
||||||
|
|
||||||
|
// Sanitize the url
|
||||||
|
string comicURL = comicUrlMatch.captures[0];
|
||||||
|
comicURL = split(comicURL, " ")[0].replace("\"", "");
|
||||||
|
|
||||||
|
// Get the first manga page to extract the json with the page infos
|
||||||
|
string mangaPageHTML = to!string(get(comicURL));
|
||||||
|
|
||||||
|
// Get the json data of the page
|
||||||
|
auto jsonMatch = match(mangaPageHTML, jsonInfoRegex);
|
||||||
|
string jsonData = jsonMatch.captures[0];
|
||||||
|
|
||||||
|
// Sanitize json
|
||||||
|
jsonData = split(jsonData, "=")[1];
|
||||||
|
|
||||||
|
return getUrlsFromJson(jsonData);
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+
|
||||||
|
+/
|
||||||
|
public this(Config config)
|
||||||
|
{
|
||||||
|
super(config);
|
||||||
|
}
|
||||||
|
}
|
156
source/sites/nhentai.d
Normal file
156
source/sites/nhentai.d
Normal file
@ -0,0 +1,156 @@
|
|||||||
|
module sites.nhentai;
|
||||||
|
|
||||||
|
import config.downloaderconfig;
|
||||||
|
import sites.basesite;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This class handles downloads for the site `nhentai`
|
||||||
|
+/
|
||||||
|
class NHentai : BaseSite
|
||||||
|
{
|
||||||
|
private
|
||||||
|
import std.conv : to;
|
||||||
|
import std.net.curl : get;
|
||||||
|
import std.json : JSONValue, parseJSON;
|
||||||
|
import std.array : split;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This struct holds all the needed infos about the nhentai doujin
|
||||||
|
+/
|
||||||
|
struct NHentai_Doujin_Info
|
||||||
|
{
|
||||||
|
/++
|
||||||
|
+ This is the number of the manga
|
||||||
|
+/
|
||||||
|
string number;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This is the title of the the manga
|
||||||
|
+/
|
||||||
|
string title;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This array holds all the urls of the images
|
||||||
|
+/
|
||||||
|
string[] imageUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This is the url of the nhentai api
|
||||||
|
+ calls are made by the number of the manga
|
||||||
|
+ for example "https://apis.nhent.ai/g/1"
|
||||||
|
+
|
||||||
|
+ The returned json string contains all the info
|
||||||
|
+ should be read into `NHenta_Doujin_Info`
|
||||||
|
+/
|
||||||
|
immutable string api_url = "https://apis.nhent.ai/g/";
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This variable holds the class internal
|
||||||
|
+ number of the manga
|
||||||
|
+/
|
||||||
|
string _number;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This struct contains all the needed infos
|
||||||
|
+ to download the managa
|
||||||
|
+/
|
||||||
|
NHentai_Doujin_Info _nhentai_doujin_info;
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function extracts the number of the manga
|
||||||
|
+ from the supplied url
|
||||||
|
+/
|
||||||
|
string extractNumFromUrl(string url)
|
||||||
|
{
|
||||||
|
string[] tmpString = url.split("/");
|
||||||
|
// FIXME: length could be unsigned so substract bad!
|
||||||
|
return tmpString[tmpString.length-2];
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This function gets the info of of the doujin using the api
|
||||||
|
+ it returns a struct with all the important info
|
||||||
|
+/
|
||||||
|
NHentai_Doujin_Info getDoujinInfo(string mangaNum)
|
||||||
|
{
|
||||||
|
NHentai_Doujin_Info _info;
|
||||||
|
|
||||||
|
// Craft the url
|
||||||
|
string requestUrl = api_url ~ mangaNum;
|
||||||
|
|
||||||
|
// Get the json data for the manga
|
||||||
|
string jsonData = to!string(get(requestUrl));
|
||||||
|
|
||||||
|
// Extract the image urls from the json string
|
||||||
|
_info.imageUrls = getUrlsFromJson(jsonData);
|
||||||
|
|
||||||
|
// Parse the data
|
||||||
|
auto parseData = parseJSON(jsonData);
|
||||||
|
|
||||||
|
// Get the title
|
||||||
|
_info.title = parseData["title"].str();
|
||||||
|
|
||||||
|
return _info;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ If the class internal info struct is filled
|
||||||
|
+ but the number is different `getDoujinInfo` gets
|
||||||
|
+ called otherwise nothing happens
|
||||||
|
+/
|
||||||
|
void fetchInfoForManaga(string number)
|
||||||
|
{
|
||||||
|
// If the doujin info wasnt fetched fetch it now
|
||||||
|
if(_nhentai_doujin_info.number != number)
|
||||||
|
{
|
||||||
|
writeln("\nGetting info....");
|
||||||
|
// Fill the info
|
||||||
|
_nhentai_doujin_info = getDoujinInfo(number);
|
||||||
|
_nhentai_doujin_info.number = number;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
override string getNameFromUrl(string url)
|
||||||
|
{
|
||||||
|
// Extract the manga number
|
||||||
|
_number = extractNumFromUrl(url);
|
||||||
|
|
||||||
|
// Fetch manga infos
|
||||||
|
fetchInfoForManaga(_number);
|
||||||
|
|
||||||
|
// Return the name of the managa
|
||||||
|
return _nhentai_doujin_info.title;
|
||||||
|
}
|
||||||
|
|
||||||
|
override string[] getImageUrlsFromBase(string url)
|
||||||
|
{
|
||||||
|
// Fetch info if it wanst already fetched
|
||||||
|
fetchInfoForManaga(_number);
|
||||||
|
|
||||||
|
return _nhentai_doujin_info.imageUrls;
|
||||||
|
}
|
||||||
|
|
||||||
|
override string[] getUrlsFromJson(string json)
|
||||||
|
{
|
||||||
|
// Extract url from json
|
||||||
|
string[] urls;
|
||||||
|
|
||||||
|
JSONValue parsedJson = parseJSON(json);
|
||||||
|
|
||||||
|
// Extract the urls for the images
|
||||||
|
foreach(JSONValue val; parsedJson["pages"].array())
|
||||||
|
urls ~= val.str().replace("i.bakaa.me", "i.nhentai.net");
|
||||||
|
|
||||||
|
return urls;
|
||||||
|
}
|
||||||
|
|
||||||
|
/++
|
||||||
|
+ This constructor just calls the inherited constructor
|
||||||
|
+/
|
||||||
|
public this(Config config)
|
||||||
|
{
|
||||||
|
super(config);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user