add support to download image

This commit is contained in:
2022-02-27 17:50:15 +08:00
parent 7326df4aaa
commit 23cbb89d10
19 changed files with 1043 additions and 183 deletions

15
Cargo.lock generated
View File

@@ -394,6 +394,17 @@ version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
[[package]]
name = "futures-macro"
version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.21"
@@ -413,9 +424,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [
"futures-core",
"futures-macro",
"futures-task",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
@@ -890,11 +903,13 @@ version = "0.0.1"
dependencies = [
"chrono",
"dateparser",
"futures-util",
"getopts",
"gettext",
"html_parser",
"json",
"lazy_static",
"regex",
"reqwest",
"spin_on",
"tokio",

View File

@@ -8,14 +8,16 @@ edition = "2018"
[dependencies]
chrono = "0.4"
dateparser = "0.1.6"
futures-util = "0.3"
getopts = "0.2"
gettext = "0.4"
html_parser = "0.6.2"
lazy_static = "1.4"
json = "0.12"
regex = "1"
reqwest = { version = "0.11", features = ["brotli", "deflate", "gzip", "rustls-tls", "socks", "stream"] }
spin_on = "0.1.1"
tokio = { version = "1.17", features = ["rt", "macros", "rt-multi-thread"] }
tokio = { version = "1.17", features = ["rt", "macros", "rt-multi-thread", "time"] }
[target.'cfg(windows)'.dependencies]
winapi = { version = "0.3.9", features = ["winnls", "stringapiset"] }

View File

@@ -1,15 +1,15 @@
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: bili\n"
"POT-Creation-Date: 2022-02-26 23:42+0800\n"
"Project-Id-Version: pixiv_downloader\n"
"POT-Creation-Date: 2022-02-27 17:47+0800\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <[email protected]>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0\n"
"X-Generator: Poedit 3.0.1\n"
"X-Poedit-Basepath: ../src\n"
"X-Poedit-SearchPath-0: .\n"
@@ -69,95 +69,171 @@ msgstr ""
msgid "Can not parse expired time:"
msgstr ""
#: cookies.rs:366 settings.rs:392
#: cookies.rs:366 data/json.rs:39 settings.rs:392 webclient.rs:257
msgid "Failed to remove file:"
msgstr ""
#: cookies.rs:372 settings.rs:399
#: cookies.rs:372 data/json.rs:50 settings.rs:399 webclient.rs:263
msgid "Failed to create file:"
msgstr ""
#: cookies.rs:379 settings.rs:405
#: cookies.rs:379 data/json.rs:56 settings.rs:405 webclient.rs:276
msgid "Failed to write file:"
msgstr ""
#: download.rs:9 pixiv.rs:47
#: download.rs:15 pixiv_web.rs:53
msgid "Failed to initialize pixiv web api client."
msgstr ""
#: opts.rs:69
#: download.rs:22
msgid "Warning: Web api client not logined, some future may not work."
msgstr ""
#: download.rs:45
msgid "Failed to get page count."
msgstr ""
#: download.rs:54
msgid "Failed to get pages' data."
msgstr ""
#: download.rs:61
msgid "Failed to save metadata to JSON file."
msgstr ""
#: download.rs:69
msgid "Failed to get original picture's link."
msgstr ""
#: download.rs:75
msgid "Failed to get file name from url:"
msgstr ""
#: download.rs:82 download.rs:88 pixiv_web.rs:148
msgid "Failed to download image:"
msgstr ""
#: download.rs:93
msgid "Downloaded image:"
msgstr ""
#: opts.rs:76
msgid "Warning: The specified config file not found."
msgstr ""
#: opts.rs:93
#: opts.rs:100
msgid "Usage:"
msgstr ""
#: opts.rs:95
#: opts.rs:102
msgid "Download an artwork"
msgstr ""
#: opts.rs:97
#: opts.rs:104
msgid "Fix the config file"
msgstr ""
#: opts.rs:99
#: opts.rs:106
msgid "Print all available settings"
msgstr ""
#: opts.rs:107
#: opts.rs:114
msgid "Print help message."
msgstr ""
#: opts.rs:111
#: opts.rs:118
msgid "The location of config file."
msgstr ""
#: opts.rs:117 settings_list.rs:8
#: opts.rs:124 settings_list.rs:8
msgid "The location of cookies file. Used for web API."
msgstr ""
#: opts.rs:123 settings_list.rs:9
#: opts.rs:130 settings_list.rs:9
msgid "The language of translated tags."
msgstr ""
#: opts.rs:145
#: opts.rs:133
msgid "Verbose logging."
msgstr ""
#: opts.rs:134
msgid "Overwrite existing file."
msgstr ""
#: opts.rs:135
msgid "Skip overwrite existing file."
msgstr ""
#: opts.rs:155
msgid "Unknown command."
msgstr ""
#: opts.rs:156
msgid "No URL specified."
#: opts.rs:165
msgid "Failed to parse ID:"
msgstr ""
#: opts.rs:164
#: opts.rs:171
msgid "No URL or ID specified."
msgstr ""
#: opts.rs:179
msgid "No detailed command specified."
msgstr ""
#: opts.rs:177
#: opts.rs:192
msgid "Unknown config subcommand."
msgstr ""
#: parser\mainpage.rs:53 parser/mainpage.rs:53
#: parser/metadata.rs:54 pixiv_web.rs:107
msgid "Failed to parse JSON:"
msgstr ""
#: parser\mainpage.rs:75 parser/mainpage.rs:75
#: parser/metadata.rs:76
msgid "Failed to parse HTML:"
msgstr ""
#: parser\mainpage.rs:80 parser/mainpage.rs:80
#: parser/metadata.rs:81
msgid "Some errors occured during parsing:"
msgstr ""
#: pixiv.rs:63 pixiv.rs:68
#: pixiv_web.rs:68 pixiv_web.rs:73
msgid "Failed to get main page:"
msgstr ""
#: pixiv.rs:74
#: pixiv_web.rs:79
msgid "Failed to parse main page."
msgstr ""
#: pixiv.rs:88
#: pixiv_web.rs:83
msgid "Main page's data:"
msgstr ""
#: pixiv_web.rs:98
msgid "Network error:"
msgstr ""
#: pixiv_web.rs:117
msgid "Failed to detect error."
msgstr ""
#: pixiv_web.rs:164 pixiv_web.rs:169
msgid "Failed to get artwork page:"
msgstr ""
#: pixiv_web.rs:175
msgid "Failed to parse artwork page."
msgstr ""
#: pixiv_web.rs:179
msgid "Artwork's data:"
msgstr ""
#: pixiv_web.rs:193
msgid "Artwork's page data:"
msgstr ""
#: pixiv_web.rs:217
msgid "Warning: Failed to save cookies file:"
msgstr ""
@@ -201,26 +277,54 @@ msgstr ""
msgid "Pixiv's refresh tokens. Used to login."
msgstr ""
#: webclient.rs:59
#: utils.rs:29
msgid "Do you want to delete file \"<file>\"?"
msgstr ""
#: utils.rs:49 webclient.rs:143
msgid "Can not parse URL:"
msgstr ""
#: utils.rs:56
msgid "Failed to get file name from path:"
msgstr ""
#: webclient.rs:103
msgid "Failed to parse Set-Cookie header."
msgstr ""
#: webclient.rs:64
#: webclient.rs:108
msgid "Failed to convert to string:"
msgstr ""
#: webclient.rs:94
#: webclient.rs:150
msgid "Parameters should be object or array:"
msgstr ""
#: webclient.rs:170
msgid "Parameters should be array:"
msgstr ""
#: webclient.rs:174
msgid "Parameters need at least a value:"
msgstr ""
#: webclient.rs:209
msgid "Error when request:"
msgstr ""
#: main.rs:46
#: webclient.rs:270
msgid "Error when downloading file:"
msgstr ""
#: main.rs:50
msgid "Failed to save config file:"
msgstr ""
#: main.rs:57
#: main.rs:61
msgid "All available settings:"
msgstr ""
#: main.rs:89
#: main.rs:93
msgid "Can not read config file:"
msgstr ""

View File

@@ -1,17 +1,17 @@
msgid ""
msgstr ""
"Project-Id-Version: bili\n"
"POT-Creation-Date: 2022-02-26 23:42+0800\n"
"PO-Revision-Date: 2022-02-26 23:43+0800\n"
"Project-Id-Version: pixiv_downloader\n"
"POT-Creation-Date: 2022-02-27 17:47+0800\n"
"PO-Revision-Date: 2022-02-27 17:48+0800\n"
"Last-Translator: lifegpc <[email protected]>\n"
"Language-Team: \n"
"Language: zh_CN\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"X-Generator: Poedit 3.0\n"
"X-Poedit-Basepath: ../src\n"
"Plural-Forms: nplurals=1; plural=0;\n"
"X-Generator: Poedit 3.0.1\n"
"X-Poedit-Basepath: ../src\n"
"X-Poedit-SearchPath-0: .\n"
#: cookies.rs:75 cookies.rs:217
@@ -70,95 +70,171 @@ msgstr "无效的Cookie:"
msgid "Can not parse expired time:"
msgstr "无法解析过期时间:"
#: cookies.rs:366 settings.rs:392
#: cookies.rs:366 data/json.rs:39 settings.rs:392 webclient.rs:257
msgid "Failed to remove file:"
msgstr "无法删除文件:"
#: cookies.rs:372 settings.rs:399
#: cookies.rs:372 data/json.rs:50 settings.rs:399 webclient.rs:263
msgid "Failed to create file:"
msgstr "无法创建文件:"
#: cookies.rs:379 settings.rs:405
#: cookies.rs:379 data/json.rs:56 settings.rs:405 webclient.rs:276
msgid "Failed to write file:"
msgstr "无法写入文件:"
#: download.rs:9 pixiv.rs:47
#: download.rs:15 pixiv_web.rs:53
msgid "Failed to initialize pixiv web api client."
msgstr "无法初始化 Pixiv 网页 API 客户端。"
#: opts.rs:69
#: download.rs:22
msgid "Warning: Web api client not logined, some future may not work."
msgstr "警告:Web API 客户端未登录,一些功能可能无法工作。"
#: download.rs:45
msgid "Failed to get page count."
msgstr "无法获取页数。"
#: download.rs:54
msgid "Failed to get pages' data."
msgstr "无法获取每页数据。"
#: download.rs:61
msgid "Failed to save metadata to JSON file."
msgstr "无法将元数据保存到 JSON 文件。"
#: download.rs:69
msgid "Failed to get original picture's link."
msgstr "无法获取原图链接。"
#: download.rs:75
msgid "Failed to get file name from url:"
msgstr "无法从 URL 获取文件名:"
#: download.rs:82 download.rs:88 pixiv_web.rs:148
msgid "Failed to download image:"
msgstr "无法下载图片:"
#: download.rs:93
msgid "Downloaded image:"
msgstr "已下载图片:"
#: opts.rs:76
msgid "Warning: The specified config file not found."
msgstr "警告:没有找到指定的设置文件。"
#: opts.rs:93
#: opts.rs:100
msgid "Usage:"
msgstr "使用方法:"
#: opts.rs:95
#: opts.rs:102
msgid "Download an artwork"
msgstr "下载一个作品"
#: opts.rs:97
#: opts.rs:104
msgid "Fix the config file"
msgstr "修复设置文件"
#: opts.rs:99
#: opts.rs:106
msgid "Print all available settings"
msgstr "打印所有可用的设置"
#: opts.rs:107
#: opts.rs:114
msgid "Print help message."
msgstr "打印帮助信息。"
#: opts.rs:111
#: opts.rs:118
msgid "The location of config file."
msgstr "设置文件的位置。"
#: opts.rs:117 settings_list.rs:8
#: opts.rs:124 settings_list.rs:8
msgid "The location of cookies file. Used for web API."
msgstr "cookies 文件的位置。用于网页 API。"
#: opts.rs:123 settings_list.rs:9
#: opts.rs:130 settings_list.rs:9
msgid "The language of translated tags."
msgstr "翻译后的标签语言。"
#: opts.rs:145
#: opts.rs:133
msgid "Verbose logging."
msgstr "启用详细内容输出。"
#: opts.rs:134
msgid "Overwrite existing file."
msgstr "覆盖已有文件。"
#: opts.rs:135
msgid "Skip overwrite existing file."
msgstr "跳过覆盖已有文件。"
#: opts.rs:155
msgid "Unknown command."
msgstr "未知指令。"
#: opts.rs:156
msgid "No URL specified."
msgstr "没有指定网址。"
#: opts.rs:165
msgid "Failed to parse ID:"
msgstr "无法解析 ID:"
#: opts.rs:164
#: opts.rs:171
msgid "No URL or ID specified."
msgstr "没有指定网址或 ID。"
#: opts.rs:179
msgid "No detailed command specified."
msgstr "没有指定更详细的指令。"
#: opts.rs:177
#: opts.rs:192
msgid "Unknown config subcommand."
msgstr "未知的 config 子指令。"
#: parser\mainpage.rs:53 parser/mainpage.rs:53
#: parser/metadata.rs:54 pixiv_web.rs:107
msgid "Failed to parse JSON:"
msgstr "无法解析 JSON:"
#: parser\mainpage.rs:75 parser/mainpage.rs:75
#: parser/metadata.rs:76
msgid "Failed to parse HTML:"
msgstr "无法解析 HTML:"
#: parser\mainpage.rs:80 parser/mainpage.rs:80
#: parser/metadata.rs:81
msgid "Some errors occured during parsing:"
msgstr "在解析中发生了一些错误:"
#: pixiv.rs:63 pixiv.rs:68
#: pixiv_web.rs:68 pixiv_web.rs:73
msgid "Failed to get main page:"
msgstr "无法获取主页:"
#: pixiv.rs:74
#: pixiv_web.rs:79
msgid "Failed to parse main page."
msgstr "无法解析主页。"
#: pixiv.rs:88
#: pixiv_web.rs:83
msgid "Main page's data:"
msgstr "主页的数据:"
#: pixiv_web.rs:98
msgid "Network error:"
msgstr "网络错误:"
#: pixiv_web.rs:117
msgid "Failed to detect error."
msgstr "检测 error 字段失败。"
#: pixiv_web.rs:164 pixiv_web.rs:169
msgid "Failed to get artwork page:"
msgstr "无法获取作品页:"
#: pixiv_web.rs:175
msgid "Failed to parse artwork page."
msgstr "无法解析作品页。"
#: pixiv_web.rs:179
msgid "Artwork's data:"
msgstr "作品数据:"
#: pixiv_web.rs:193
msgid "Artwork's page data:"
msgstr "作品页面数据:"
#: pixiv_web.rs:217
msgid "Warning: Failed to save cookies file:"
msgstr "警告:无法保存 cookies 文件:"
@@ -206,26 +282,54 @@ msgstr "无法刷新文件缓冲区:"
msgid "Pixiv's refresh tokens. Used to login."
msgstr "Pixiv 的 refresh tokens。用于登录。"
#: webclient.rs:59
#: utils.rs:29
msgid "Do you want to delete file \"<file>\"?"
msgstr "你想要删除文件 <file> 吗?"
#: utils.rs:49 webclient.rs:143
msgid "Can not parse URL:"
msgstr "无法解析 URL:"
#: utils.rs:56
msgid "Failed to get file name from path:"
msgstr "无法从路径获取文件名:"
#: webclient.rs:103
msgid "Failed to parse Set-Cookie header."
msgstr "无法解析 Set-Cookie 头部。"
#: webclient.rs:64
#: webclient.rs:108
msgid "Failed to convert to string:"
msgstr "无法转换为字符串:"
#: webclient.rs:94
#: webclient.rs:150
msgid "Parameters should be object or array:"
msgstr "参数应该是对象或数组:"
#: webclient.rs:170
msgid "Parameters should be array:"
msgstr "参数应该是数组:"
#: webclient.rs:174
msgid "Parameters need at least a value:"
msgstr "参数需要至少含有一个值:"
#: webclient.rs:209
msgid "Error when request:"
msgstr "请求时发生错误:"
#: main.rs:46
#: webclient.rs:270
msgid "Error when downloading file:"
msgstr "下载文件时发生错误:"
#: main.rs:50
msgid "Failed to save config file:"
msgstr "无法保存设置文件:"
#: main.rs:57
#: main.rs:61
msgid "All available settings:"
msgstr "所有可用的设置:"
#: main.rs:89
#: main.rs:93
msgid "Can not read config file:"
msgstr "无法读取设置文件:"

View File

@@ -234,7 +234,7 @@ impl Cookie {
} else {
self._domain.clone()
};
if subdomain && !host.starts_with(&domain) && host != &domain[1..] {
if subdomain && !host.ends_with(&domain) && host != &domain[1..] {
return false;
}
if !subdomain && host != domain {

76
src/data/json.rs Normal file
View File

@@ -0,0 +1,76 @@
use crate::gettext;
use crate::pixiv_link::PixivID;
use crate::pixiv_link::ToPixivID;
use json::JsonValue;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::File;
use std::fs::remove_file;
use std::io::Write;
use std::path::Path;
pub trait ToJson {
fn to_json(&self) -> Option<JsonValue>;
}
/// Store metadata informations in JSON file
pub struct JSONDataFile {
id: PixivID,
maps: HashMap<String, JsonValue>,
}
impl JSONDataFile {
pub fn new<T: ToPixivID>(id: T) -> Option<Self> {
let i = id.to_pixiv_id();
if i.is_none() {
return None;
}
Some(Self {
id: i.unwrap(),
maps: HashMap::new(),
})
}
pub fn save<S: AsRef<OsStr> + ?Sized>(&self, path: &S) -> bool {
let p = Path::new(path);
if p.exists() {
let r = remove_file(p);
if r.is_err() {
println!("{} {}", gettext("Failed to remove file:"), r.unwrap_err());
return false;
}
}
let value = self.to_json();
if value.is_none() {
return false;
}
let value = value.unwrap();
let f = File::create(p);
if f.is_err() {
println!("{} {}", gettext("Failed to create file:"), f.unwrap_err());
return false;
}
let mut f = f.unwrap();
let r = f.write(value.pretty(2).as_bytes());
if r.is_err() {
println!("{} {}", gettext("Failed to write file:"), r.unwrap_err());
return false;
}
true
}
}
impl ToJson for JSONDataFile {
fn to_json(&self) -> Option<JsonValue> {
let mut value = json::object! {};
if value.insert("id", self.id.to_json()).is_err() {
return None;
}
for (k, v) in self.maps.iter() {
if value.insert(k, v.clone()).is_err() {
return None;
}
}
Some(value)
}
}

1
src/data/mod.rs Normal file
View File

@@ -0,0 +1 @@
pub mod json;

View File

@@ -1,6 +1,12 @@
use crate::data::json::JSONDataFile;
use crate::gettext;
use crate::pixiv::PixivWebClient;
use crate::pixiv_link::PixivID;
use crate::pixiv_web::PixivWebClient;
use crate::utils::get_file_name_from_url;
use crate::webclient::WebClient;
use crate::Main;
use json::JsonValue;
use std::path::PathBuf;
impl Main {
pub fn download(&mut self) -> i32 {
@@ -10,6 +16,86 @@ impl Main {
return 1;
}
pw.check_login();
if !pw.logined() {
println!(
"{}",
gettext("Warning: Web api client not logined, some future may not work.")
);
}
for id in self.cmd.as_ref().unwrap().ids.iter() {
match id {
PixivID::Artwork(id) => {
let r = self.download_artwork(&mut pw, id.clone());
if r != 0 {
return r;
}
}
}
}
0
}
pub fn download_artwork(&self, pw: &mut PixivWebClient, id: u64) -> i32 {
let re = pw.get_artwork(id);
if re.is_none() {
return 1;
}
let pages = (&(re.as_ref().unwrap())["illust"][format!("{}", id).as_str()]["sl"]).as_u64();
if pages.is_none() {
println!("{}", gettext("Failed to get page count."));
return 1;
}
let pages = pages.unwrap();
let mut pages_data: Option<JsonValue> = None;
if pages > 1 {
pages_data = pw.get_illust_pages(id);
}
if pages > 1 && pages_data.is_none() {
println!("{}", gettext("Failed to get pages' data."));
return 1;
}
let base = PathBuf::from(".");
let json_file = base.join(format!("{}.json", id));
let json_data = JSONDataFile::new(id).unwrap();
if !json_data.save(&json_file) {
println!("{}", gettext("Failed to save metadata to JSON file."));
return 1;
}
if pages_data.is_some() {
let pages_data = pages_data.as_ref().unwrap();
for page in pages_data.members() {
let link = &page["urls"]["original"];
if !link.is_string() {
println!("{}", gettext("Failed to get original picture's link."));
return 1;
}
let link = link.as_str().unwrap();
let file_name = get_file_name_from_url(link);
if file_name.is_none() {
println!("{} {}", gettext("Failed to get file name from url:"), link);
return 1;
}
let file_name = file_name.unwrap();
let file_name = base.join(file_name);
let r = pw.download_image(link);
if r.is_none() {
println!("{} {}", gettext("Failed to download image:"), link);
return 1;
}
let r = r.unwrap();
let re = WebClient::download_stream(&file_name, r, pw.helper.overwrite());
if re.is_err() {
println!("{} {}", gettext("Failed to download image:"), link);
return 1;
}
println!(
"{} {} -> {}",
gettext("Downloaded image:"),
link,
file_name.to_str().unwrap_or("(null)")
)
}
}
0
}
}

View File

@@ -150,6 +150,7 @@ impl I18n {
}
lazy_static! {
#[doc(hidden)]
static ref I18NT: I18n = I18n::new();
}

View File

@@ -1,18 +1,22 @@
extern crate chrono;
extern crate dateparser;
extern crate futures_util;
extern crate json;
#[macro_use]
extern crate lazy_static;
extern crate tokio;
extern crate regex;
extern crate reqwest;
mod cookies;
mod data;
mod download;
mod i18n;
mod opthelper;
mod opts;
mod parser;
mod pixiv;
mod pixiv_link;
mod pixiv_web;
mod settings;
mod settings_list;
mod utils;

View File

@@ -1,13 +1,17 @@
use crate::opts::CommandOpts;
use crate::settings::SettingStore;
/// An sturct to access all available settings/command line switches
#[derive(Clone)]
pub struct OptHelper<'a> {
/// Command Line Options
opt: &'a CommandOpts,
/// Settings
settings: &'a SettingStore,
}
impl<'a> OptHelper<'a> {
/// return cookies location, no any check
pub fn cookies(&self) -> Option<String> {
if self.opt.cookies.is_some() {
self.opt.cookies.clone()
@@ -18,6 +22,7 @@ impl<'a> OptHelper<'a> {
}
}
/// return language
pub fn language(&self) -> Option<String> {
if self.opt.language.is_some() {
self.opt.language.clone()
@@ -31,4 +36,12 @@ impl<'a> OptHelper<'a> {
pub fn new(opt: &'a CommandOpts, settings: &'a SettingStore) -> Self {
Self { opt, settings }
}
pub fn overwrite(&self) -> Option<bool> {
self.opt.overwrite
}
pub fn verbose(&self) -> bool {
self.opt.verbose
}
}

View File

@@ -1,6 +1,7 @@
extern crate getopts;
use crate::gettext;
use crate::pixiv_link::PixivID;
use crate::utils::check_file_exists;
use crate::utils::get_exe_path_else_current;
use getopts::Options;
@@ -35,8 +36,8 @@ impl PartialEq<ConfigCommand> for &ConfigCommand {
pub struct CommandOpts {
/// Command
pub cmd: Command,
/// URLs
pub urls: Vec<String>,
/// IDs
pub ids: Vec<PixivID>,
/// Config location
pub _config: Option<String>,
/// Config command
@@ -45,17 +46,23 @@ pub struct CommandOpts {
pub cookies: Option<String>,
/// The language of translated tags
pub language: Option<String>,
/// Verbose logging
pub verbose: bool,
/// Whether to overwrite file
pub overwrite: Option<bool>,
}
impl CommandOpts {
pub fn new(cmd: Command) -> Self {
Self {
cmd,
urls: Vec::new(),
ids: Vec::new(),
_config: None,
config_cmd: None,
cookies: None,
language: None,
verbose: false,
overwrite: None,
}
}
@@ -87,7 +94,7 @@ impl CommandOpts {
pub fn print_usage(prog: &str, opts: &Options) {
let brief = format!(
"{}
{} download [options] <url> [<url>] {}
{} download [options] <id/url> [<id/url>] {}
{} config fix [options] {}
{} config help [options] {}",
gettext("Usage:"),
@@ -123,6 +130,9 @@ pub fn parse_cmd() -> Option<CommandOpts> {
gettext("The language of translated tags."),
"LANG",
);
opts.optflag("v", "verbose", gettext("Verbose logging."));
opts.optflag("y", "yes", gettext("Overwrite existing file."));
opts.optflag("n", "no", gettext("Skip overwrite existing file."));
let result = match opts.parse(&argv[1..]) {
Ok(m) => m,
Err(err) => {
@@ -148,16 +158,21 @@ pub fn parse_cmd() -> Option<CommandOpts> {
}
match re.as_ref().unwrap().cmd {
Command::Download => {
let mut urls = Vec::new();
let mut ids = Vec::new();
for url in result.free.iter().skip(1) {
urls.push(url.to_string());
let id = PixivID::parse(url);
if id.is_none() {
println!("{} {}", gettext("Failed to parse ID:"), url);
return None;
}
ids.push(id.unwrap());
}
if urls.is_empty() {
println!("{}", gettext("No URL specified."));
if ids.is_empty() {
println!("{}", gettext("No URL or ID specified."));
print_usage(&argv[0], &opts);
return None;
}
re.as_mut().unwrap().urls = urls;
re.as_mut().unwrap().ids = ids;
}
Command::Config => {
if result.free.len() < 2 {
@@ -189,5 +204,21 @@ pub fn parse_cmd() -> Option<CommandOpts> {
if result.opt_present("language") {
re.as_mut().unwrap().language = Some(result.opt_str("language").unwrap());
}
re.as_mut().unwrap().verbose = result.opt_present("verbose");
let yes = result.opt_present("yes");
let no = result.opt_present("no");
re.as_mut().unwrap().overwrite = if yes && no {
if result.opt_positions("yes").last().unwrap() > result.opt_positions("no").last().unwrap() {
Some(true)
} else {
Some(false)
}
} else if yes {
Some(true)
} else if no {
Some(false)
} else {
None
};
re
}

View File

@@ -2,14 +2,19 @@ use crate::gettext;
use html_parser::Dom;
use html_parser::Node;
use json::JsonValue;
use std::default::Default;
pub struct MainPageParser {
pub struct MetaDataParser {
pub key: String,
pub value: Option<JsonValue>,
}
impl MainPageParser {
pub fn new() -> Self {
impl MetaDataParser {
/// Create an new instance
/// * `key` - meta's name
pub fn new(key: &str) -> Self {
Self {
key: key.to_string(),
value: None,
}
}
@@ -26,13 +31,14 @@ impl MainPageParser {
if name.is_none() {
return false;
}
if name.as_ref().unwrap() != "global-data" {
if name.as_ref().unwrap() != self.key.as_str() {
return false;
}
if e.id.is_none() {
return false;
}
if e.id.as_ref().unwrap() != "meta-global-data" {
let mkey = format!("meta-{}", self.key.as_str());
if e.id.as_ref().unwrap() != mkey.as_str() {
return false;
}
let c = e.attributes.get("content");
@@ -85,3 +91,9 @@ impl MainPageParser {
false
}
}
impl Default for MetaDataParser {
fn default() -> Self {
Self::new("global-data")
}
}

View File

@@ -1,3 +1,3 @@
extern crate html_parser;
pub mod mainpage;
pub mod metadata;

View File

@@ -1,91 +0,0 @@
use crate::gettext;
use crate::opthelper::OptHelper;
use crate::parser::mainpage::MainPageParser;
use crate::webclient::WebClient;
use crate::Main;
use spin_on::spin_on;
/// A client which use Pixiv's web API
pub struct PixivWebClient<'a> {
client: WebClient,
helper: OptHelper<'a>,
/// true if in is initialized
inited: bool,
}
impl<'a> PixivWebClient<'a> {
pub fn new(m: &'a Main) -> Self {
Self {
client: WebClient::new(),
helper: OptHelper::new(m.cmd.as_ref().unwrap(), m.settings.as_ref().unwrap()),
inited: false,
}
}
pub fn init(&mut self) -> bool {
let c = self.helper.cookies();
if c.is_some() {
if !self.client.read_cookies(c.as_ref().unwrap()) {
return false;
}
}
self.client.set_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36");
let l = self.helper.language();
if l.is_some() {
self.client.set_header("Accept-Language", l.as_ref().unwrap());
} else {
self.client.set_header("Accept-Language", "ja");
}
self.inited = true;
true
}
pub fn auto_init(&mut self) {
if !self.inited {
let r = self.init();
if !r {
panic!("{}", gettext("Failed to initialize pixiv web api client."));
}
}
}
pub fn check_login(&mut self) -> bool {
self.auto_init();
let r = self.client.get("https://www.pixiv.net/");
if r.is_none() {
return false;
}
let r = r.unwrap();
let status = r.status();
let code = status.as_u16();
if code >= 400 {
println!("{} {}", gettext("Failed to get main page:"), status);
return false;
}
let data = spin_on(r.text_with_charset("UTF-8"));
if data.is_err() {
println!("{} {}", gettext("Failed to get main page:"), data.unwrap_err());
return false;
}
let data = data.unwrap();
let mut p = MainPageParser::new();
if !p.parse(data.as_str()) {
println!("{}", gettext("Failed to parse main page."));
return false;
}
true
}
}
impl <'a> Drop for PixivWebClient<'a> {
fn drop(&mut self) {
if self.inited {
let c = self.helper.cookies();
if c.is_some() {
if !self.client.save_cookies(c.as_ref().unwrap()) {
println!("{} {}", gettext("Warning: Failed to save cookies file:"), c.as_ref().unwrap());
}
}
}
}
}

73
src/pixiv_link.rs Normal file
View File

@@ -0,0 +1,73 @@
use crate::data::json::ToJson;
use json::JsonValue;
use regex::Regex;
lazy_static! {
#[doc(hidden)]
static ref RE: Regex = Regex::new("^(https?://)?(www\\.)?pixiv\\.net/artworks/(?P<id>\\d+)").unwrap();
}
/// Repesent an Pixiv ID
#[derive(Clone, Debug)]
pub enum PixivID {
/// Artwork Id, include illust and manga
Artwork(u64),
}
pub trait ToPixivID {
fn to_pixiv_id(&self) -> Option<PixivID>;
}
impl PixivID {
pub fn parse(s: &str) -> Option<PixivID> {
let p = s.trim();
let num = p.parse::<u64>();
if num.is_ok() {
return Some(PixivID::Artwork(num.unwrap()));
}
let re = RE.captures(s);
if re.is_some() {
let r = re.unwrap().name("id");
if r.is_some() {
let r = r.unwrap().as_str();
let num = r.parse::<u64>();
return Some(PixivID::Artwork(num.unwrap()));
}
}
None
}
}
impl ToJson for PixivID {
fn to_json(&self) -> Option<JsonValue> {
match *self {
PixivID::Artwork(id) => {
Some(json::value!({"type": "artwork", "id": id}))
}
}
}
}
impl ToPixivID for &PixivID {
fn to_pixiv_id(&self) -> Option<PixivID> {
Some((*self).clone())
}
}
impl ToPixivID for &str {
fn to_pixiv_id(&self) -> Option<PixivID> {
PixivID::parse(*self)
}
}
impl ToPixivID for String {
fn to_pixiv_id(&self) -> Option<PixivID> {
PixivID::parse(self)
}
}
impl ToPixivID for u64 {
fn to_pixiv_id(&self) -> Option<PixivID> {
Some(PixivID::Artwork(*self))
}
}

222
src/pixiv_web.rs Normal file
View File

@@ -0,0 +1,222 @@
use crate::gettext;
use crate::opthelper::OptHelper;
use crate::parser::metadata::MetaDataParser;
use crate::webclient::WebClient;
use crate::Main;
use json::JsonValue;
use reqwest::IntoUrl;
use reqwest::Response;
use spin_on::spin_on;
/// A client which use Pixiv's web API
pub struct PixivWebClient<'a> {
client: WebClient,
pub helper: OptHelper<'a>,
/// true if in is initialized
inited: bool,
data: Option<JsonValue>,
}
impl<'a> PixivWebClient<'a> {
pub fn new(m: &'a Main) -> Self {
Self {
client: WebClient::new(),
helper: OptHelper::new(m.cmd.as_ref().unwrap(), m.settings.as_ref().unwrap()),
inited: false,
data: None,
}
}
pub fn init(&mut self) -> bool {
let c = self.helper.cookies();
if c.is_some() {
if !self.client.read_cookies(c.as_ref().unwrap()) {
return false;
}
}
self.client.set_header("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36");
let l = self.helper.language();
if l.is_some() {
self.client.set_header("Accept-Language", l.as_ref().unwrap());
} else {
self.client.set_header("Accept-Language", "ja");
}
self.client.verbose = self.helper.verbose();
self.inited = true;
true
}
pub fn auto_init(&mut self) {
if !self.inited {
let r = self.init();
if !r {
panic!("{}", gettext("Failed to initialize pixiv web api client."));
}
}
}
pub fn check_login(&mut self) -> bool {
self.auto_init();
let r = self.client.get("https://www.pixiv.net/", None);
if r.is_none() {
return false;
}
let r = r.unwrap();
let status = r.status();
let code = status.as_u16();
if code >= 400 {
println!("{} {}", gettext("Failed to get main page:"), status);
return false;
}
let data = spin_on(r.text_with_charset("UTF-8"));
if data.is_err() {
println!("{} {}", gettext("Failed to get main page:"), data.unwrap_err());
return false;
}
let data = data.unwrap();
let mut p = MetaDataParser::default();
if !p.parse(data.as_str()) {
println!("{}", gettext("Failed to parse main page."));
return false;
}
if self.helper.verbose() {
println!("{}\n{}", gettext("Main page's data:"), p.value.as_ref().unwrap().pretty(2).as_str());
}
self.data = Some(p.value.unwrap());
true
}
pub fn deal_json(&mut self, r: Response) -> Option<JsonValue> {
let status = r.status();
let code = status.as_u16();
let is_status_err = code >= 400;
let data = spin_on(r.text_with_charset("UTF-8"));
if data.is_err() {
if is_status_err {
println!("HTTP ERROR {}", status);
}
println!("{} {}", gettext("Network error:"), data.unwrap_err());
return None;
}
let data = data.unwrap();
let re = json::parse(data.as_str());
if re.is_err() {
if is_status_err {
println!("HTTP ERROR {}", status);
} else {
println!("{} {}", gettext("Failed to parse JSON:"), re.unwrap_err());
}
return None;
}
let value = re.unwrap();
let error = (&value["error"]).as_bool();
if error.is_none() {
if is_status_err {
println!("HTTP ERROR {}", status);
}
println!("{}", gettext("Failed to detect error."));
return None;
}
let error = error.unwrap();
if error {
let message = (&value["message"]).as_str();
if is_status_err {
println!("HTTP ERROR {}", status);
}
if message.is_some() {
println!("{}", message.unwrap());
}
return None;
}
let body = &value["body"];
if body.is_empty() || body.is_null() {
return Some(value);
}
Some(body.clone())
}
pub fn download_image<U: IntoUrl>(&mut self, url: U) -> Option<Response> {
self.auto_init();
let r = self.client.get(url, json::object!{"referer": "https://www.pixiv.net/"});
if r.is_none() {
return None;
}
let r = r.unwrap();
let status = r.status();
let code = status.as_u16();
if code >= 400 {
println!("{} {}", gettext("Failed to download image:"), status);
return None;
}
Some(r)
}
pub fn get_artwork(&mut self, id: u64) -> Option<JsonValue> {
self.auto_init();
let r = self.client.get(format!("https://www.pixiv.net/artworks/{}", id), None);
if r.is_none() {
return None;
}
let r = r.unwrap();
let status = r.status();
let code = status.as_u16();
if code >= 400 {
println!("{} {}", gettext("Failed to get artwork page:"), status);
return None;
}
let data = spin_on(r.text_with_charset("UTF-8"));
if data.is_err() {
println!("{} {}", gettext("Failed to get artwork page:"), data.unwrap_err());
return None;
}
let data = data.unwrap();
let mut p = MetaDataParser::new("preload-data");
if !p.parse(data.as_str()) {
println!("{}", gettext("Failed to parse artwork page."));
return None;
}
if self.helper.verbose() {
println!("{} {}", gettext("Artwork's data:"), p.value.as_ref().unwrap().pretty(2));
}
Some(p.value.unwrap())
}
pub fn get_illust_pages(&mut self, id: u64) -> Option<JsonValue> {
self.auto_init();
let r = self.client.get(format!("https://www.pixiv.net/ajax/illust/{}/pages", id), None);
if r.is_none() {
return None;
}
let r = r.unwrap();
let v = self.deal_json(r);
if self.helper.verbose() {
println!("{} {}", gettext("Artwork's page data:"), v.as_ref().unwrap().pretty(2));
}
v
}
pub fn logined(&self) -> bool {
if self.data.is_none() {
return false;
}
let value = self.data.as_ref().unwrap();
let d = &value["userData"];
if d.is_object() {
return true;
}
false
}
}
impl <'a> Drop for PixivWebClient<'a> {
fn drop(&mut self) {
if self.inited {
let c = self.helper.cookies();
if c.is_some() {
if !self.client.save_cookies(c.as_ref().unwrap()) {
println!("{} {}", gettext("Warning: Failed to save cookies file:"), c.as_ref().unwrap());
}
}
}
}
}

View File

@@ -1,3 +1,5 @@
use crate::gettext;
use reqwest::IntoUrl;
use std::env;
use std::path::Path;
use std::path::PathBuf;
@@ -22,3 +24,41 @@ pub fn check_file_exists(path: &str) -> bool {
let p = Path::new(path);
p.exists()
}
pub fn ask_need_overwrite(path: &str) -> bool {
let s = gettext("Do you want to delete file \"<file>\"?").replace("<file>", path);
print!("{}(y/n)", s.as_str());
let mut d = String::from("");
loop {
let re = std::io::stdin().read_line(&mut d);
if re.is_err() {
continue;
}
let d = d.trim().to_lowercase();
if d == "y" {
return true;
} else {
return false;
}
}
}
pub fn get_file_name_from_url<U: IntoUrl>(url: U) -> Option<String> {
let u = url.into_url();
if u.is_err() {
println!("{} {}", gettext("Can not parse URL:"), u.unwrap_err());
return None;
}
let u = u.unwrap();
let path = Path::new(u.path());
let re = path.file_name();
if re.is_none() {
println!("{} {}", gettext("Failed to get file name from path:"), u.path());
return None;
}
let r = re.unwrap().to_str();
if r.is_none() {
return None;
}
Some(String::from(r.unwrap()))
}

View File

@@ -3,9 +3,51 @@ extern crate spin_on;
use crate::cookies::Cookie;
use crate::cookies::CookieJar;
use crate::gettext;
use crate::utils::ask_need_overwrite;
use futures_util::StreamExt;
use json::JsonValue;
use reqwest::{Client, IntoUrl, RequestBuilder, Response};
use std::collections::HashMap;
use spin_on::spin_on;
use std::collections::HashMap;
use std::ffi::OsStr;
use std::fs::File;
use std::fs::remove_file;
use std::io::Write;
use std::path::Path;
pub trait ToHeaders {
fn to_headers(&self) -> Option<HashMap<String, String>>;
}
impl ToHeaders for Option<HashMap<String, String>> {
fn to_headers(&self) -> Option<HashMap<String, String>> {
self.clone()
}
}
impl ToHeaders for HashMap<String, String> {
fn to_headers(&self) -> Option<HashMap<String, String>> {
Some(self.clone())
}
}
impl ToHeaders for JsonValue {
fn to_headers(&self) -> Option<HashMap<String, String>> {
if !self.is_object() {
return None;
}
let mut h = HashMap::new();
for (k, v) in self.entries() {
let d = if v.is_string() {
String::from(v.as_str().unwrap())
} else {
v.dump()
};
h.insert(String::from(k), d);
}
Some(h)
}
}
/// Generate `cookie` header for a url
/// * `c` - Cookies
@@ -31,6 +73,7 @@ pub struct WebClient {
/// HTTP Headers
pub headers: HashMap<String, String>,
cookies: CookieJar,
pub verbose: bool,
}
impl WebClient {
@@ -39,6 +82,7 @@ impl WebClient {
client: Client::new(),
headers: HashMap::new(),
cookies: CookieJar::new(),
verbose: false,
}
}
@@ -82,10 +126,81 @@ impl WebClient {
pub fn set_header(&mut self, key: &str, value: &str) -> Option<String> {
self.headers.insert(String::from(key), String::from(value))
}
/// Send GET requests with parameters
/// * `param` - GET parameters. Should be a JSON object/array. If value in map is not a string, will dump it
/// # Examples
/// ```
/// let client = WebClient::new();
/// client.verbose = true;
/// client.get_with_param("https://test.com/a", json::object!{"data": "param1"});
/// client.get_with_param("https://test.com/a", json::object!{"daa": {"ad": "test"}});
/// client.get_with_param("https://test.com/a", json::array![["daa", "param1"]]);
/// ```
/// It will GET `https://test.com/a?data=param1`, `https://test.com/a?daa=%7B%22ad%22%3A%22test%22%7D`, `https://test.com/a?daa=param1`
pub fn get_with_param<U: IntoUrl>(&mut self, url: U, param: JsonValue) -> Option<Response> {
let u = url.into_url();
if u.is_err() {
println!("{} \"{}\"", gettext("Can not parse URL:"), u.unwrap_err());
return None;
}
let mut u = u.unwrap();
if !param.is_object() && !param.is_array() {
println!(
"{} \"{}\"",
gettext("Parameters should be object or array:"),
param
);
return None;
}
{
let mut query = u.query_pairs_mut();
if param.is_object() {
for (k, v) in param.entries() {
let s: String;
if v.is_string() {
s = String::from(v.as_str().unwrap());
} else {
s = v.dump();
}
query.append_pair(k, s.as_str());
}
} else {
for v in param.members() {
if !v.is_object() {
println!("{} \"{}\"", gettext("Parameters should be array:"), v);
return None;
}
if v.len() < 2 {
println!("{} \"{}\"", gettext("Parameters need at least a value:"), v);
return None;
}
let okey = &v[0];
let key: String;
if okey.is_string() {
key = String::from(okey.as_str().unwrap());
} else {
key = okey.dump();
}
let mut mems = v.members();
mems.next();
for val in mems {
let s: String;
if val.is_string() {
s = String::from(val.as_str().unwrap());
} else {
s = val.dump();
}
query.append_pair(key.as_str(), s.as_str());
}
}
}
}
self.get(u.as_str(), None)
}
/// Send GET requests
pub fn get<U: IntoUrl>(&mut self, url: U) -> Option<Response> {
let r = self.aget(url);
pub fn get<U: IntoUrl, H: ToHeaders>(&mut self, url: U, headers: H) -> Option<Response> {
let r = self.aget(url, headers);
let r = r.send();
let r = spin_on(r);
match r {
@@ -97,19 +212,71 @@ impl WebClient {
}
let r = r.unwrap();
self.handle_set_cookie(&r);
if self.verbose {
println!("{}", r.status());
}
Some(r)
}
pub fn aget<U: IntoUrl>(&mut self, url: U) -> RequestBuilder {
pub fn aget<U: IntoUrl, H: ToHeaders>(&mut self, url: U, headers: H) -> RequestBuilder {
let s = url.as_str();
if self.verbose {
println!("GET {}", s);
}
let mut r = self.client.get(s);
for (k, v) in self.headers.iter() {
r = r.header(k, v);
}
let headers = headers.to_headers();
if headers.is_some() {
let h = headers.unwrap();
for (k, v) in h.iter() {
r = r.header(k, v);
}
}
let c = gen_cookie_header(&mut self.cookies, s);
if c.len() > 0 {
r = r.header("Cookie", c.as_str());
}
r
}
pub fn download_stream<S: AsRef<OsStr> + ?Sized>(file_name: &S, r: Response, overwrite: Option<bool>) -> Result<(), ()> {
let p = Path::new(file_name);
if p.exists() {
let overwrite = if overwrite.is_none() {
ask_need_overwrite(p.to_str().unwrap())
} else {
overwrite.unwrap()
};
if !overwrite {
return Ok(());
}
let re = remove_file(p);
if re.is_err() {
println!("{} {}", gettext("Failed to remove file:"), re.unwrap_err());
return Err(());
}
}
let f = File::create(p);
if f.is_err() {
println!("{} {}", gettext("Failed to create file:"), f.unwrap_err());
return Err(());
}
let mut f = f.unwrap();
let mut stream = r.bytes_stream();
while let Some(data) = spin_on(stream.next()) {
if data.is_err() {
println!("{} {}", gettext("Error when downloading file:"), data.unwrap_err());
return Err(());
}
let data = data.unwrap();
let r = f.write(&data);
if r.is_err() {
println!("{} {}", gettext("Failed to write file:"), r.unwrap_err());
return Err(());
}
}
Ok(())
}
}