This commit is contained in:
2022-02-26 23:56:33 +08:00
commit 48e0a80deb
19 changed files with 4442 additions and 0 deletions

389
src/cookies.rs Normal file
View File

@@ -0,0 +1,389 @@
use crate::gettext;
use chrono::DateTime;
use chrono::TimeZone;
use chrono::Utc;
use reqwest::IntoUrl;
use std::fs::{remove_file, File};
use std::io::BufRead;
use std::io::BufReader;
use std::io::Write;
use std::iter::Iterator;
use std::path::Path;
trait ToNetscapeStr {
fn to_netscape_str(&self) -> &'static str;
}
impl ToNetscapeStr for bool {
fn to_netscape_str(&self) -> &'static str {
if *self {
"TRUE"
} else {
"FALSE"
}
}
}
#[derive(Clone, PartialEq, Debug)]
/// Cookies structure
pub struct Cookie {
/// Cookie's name
_name: String,
/// Cookie's value
_value: String,
/// Whether to include subdomains
_subdomains: bool,
/// Cookie's Path
_path: String,
/// HTTP only
_http_only: bool,
/// Expired time
_expired: Option<DateTime<Utc>>,
/// Domain name
_domain: String,
}
impl Cookie {
pub fn new(
name: &str,
value: &str,
domain: &str,
subdomains: bool,
path: &str,
http_only: bool,
expired: Option<DateTime<Utc>>,
) -> Self {
Self {
_name: name.to_string(),
_value: value.to_string(),
_subdomains: subdomains,
_path: path.to_string(),
_http_only: http_only,
_expired: expired.clone(),
_domain: domain.to_string(),
}
}
pub fn from_set_cookie<U: IntoUrl>(url: U, header: &str) -> Option<Self> {
let mut subdomain = false;
let mut http_only = false;
let mut expired: i64 = 0;
let u = url.into_url();
if u.is_err() {
println!(
"{} {}",
gettext("Warning: Failed to parse URL:"),
u.unwrap_err()
);
return None;
}
let u = u.unwrap();
let mut path = u.path().to_string();
let t = String::from(header);
let l: Vec<&str> = t.split(";").collect();
let m = l[0];
let t = String::from(m);
let l2: Vec<&str> = t.split("=").collect();
if l2.len() < 2 {
println!(
"{} {}",
gettext("Warning: Failed to parse cookie's key and value:"),
m
);
return None;
}
let key = l2[0];
let value = l2[1];
let mut domain = match u.host_str() {
Some(s) => Some(String::from(s)),
None => None,
};
for v in l.iter().skip(2) {
let t = String::from(*v).trim().to_string();
let ll: Vec<&str> = t.split("=").collect();
let k = ll[0].to_lowercase();
if k == "expires" {
if ll.len() < 2 {
println!("{}", gettext("Warning: Expires need a date."));
return None;
}
let mut re = dateparser::parse(ll[1]);
if re.is_err() {
let s = ll[1].replace("-", " ");
re = dateparser::parse(s.as_str());
if re.is_err() {
println!(
"{} {}",
gettext("Failed to parse UTC string:"),
re.unwrap_err()
);
return None;
}
}
let r = re.unwrap();
expired = r.timestamp();
} else if k == "max-age" {
if ll.len() < 2 {
println!("{}", gettext("Warning: Max-Age need a duration."));
return None;
}
let re = ll[1].parse::<i64>();
if re.is_err() {
println!(
"{} {}",
gettext("Failed to parse Max-Age:"),
re.unwrap_err()
);
return None;
}
expired = re.unwrap() + Utc::now().timestamp();
} else if k == "domain" {
if ll.len() < 2 {
println!("{}", gettext("Warning: Domain need a domain."));
return None;
}
domain = Some(String::from(ll[1]));
} else if k == "path" {
if ll.len() < 2 {
println!("{}", gettext("Warning: Path need a path."));
return None;
}
let p = ll[1].to_string();
if !p.starts_with("/") {
println!(
"{} {}",
gettext("Warning: path is not starts with \"/\":"),
p.as_str()
);
return None;
}
path = p;
} else if k == "httponly" {
http_only = true;
} else if k == "secure" || k == "samesite" {
continue;
}
}
if domain.is_none() {
println!("{}", gettext("Warning: Failed to get domain."));
return None;
}
let domain = domain.unwrap();
if domain.starts_with(".") {
subdomain = true;
}
let expired = if expired == 0 {
None
} else {
Some(Utc.timestamp(expired, 0))
};
Some(Self::new(
key,
value,
domain.as_str(),
subdomain,
path.as_str(),
http_only,
expired,
))
}
/// Get name and value string: name=value;
pub fn get_name_value(&self) -> String {
format!("{}={};", self._name.as_str(), self._value.as_str())
}
pub fn is_expired(&self) -> bool {
if self._expired.is_some() {
let now = Utc::now();
if now > self._expired.as_ref().unwrap().clone() {
return true;
}
}
false
}
pub fn is_same_key(&self, other: &Self) -> bool {
self._name == other._name && self._domain == other._domain
}
/// Check if url is matched
/// * `url` - URL
pub fn matched<U: IntoUrl>(&self, url: U) -> bool {
let u = url.into_url();
if u.is_err() {
println!(
"{} {}",
gettext("Warning: Failed to parse URL:"),
u.unwrap_err()
);
return false;
}
if self.is_expired() {
return false;
}
let u = u.unwrap();
let host = u.host_str();
if host.is_none() {
return false;
}
let host = host.unwrap();
let subdomain = self._subdomains || self._domain.starts_with(".");
let domain = if subdomain && !self._domain.starts_with(".") {
String::from(".") + &self._domain
} else {
self._domain.clone()
};
if subdomain && !host.starts_with(&domain) && host != &domain[1..] {
return false;
}
if !subdomain && host != domain {
return false;
}
let path = u.path();
if !path.starts_with(&self._path) {
return false;
}
true
}
pub fn expired_time(&self) -> i64 {
match &self._expired {
Some(k) => k.timestamp(),
None => 0,
}
}
pub fn to_netscape_str(&self) -> String {
format!(
"{}\t{}\t{}\t{}\t{}\t{}\t{}\n",
self._domain,
self._subdomains.to_netscape_str(),
self._path,
self._http_only.to_netscape_str(),
self.expired_time(),
self._name,
self._value
)
}
}
#[derive(Clone, Debug)]
/// Cookies Jar
pub struct CookieJar {
cookies: Vec<Cookie>,
}
impl CookieJar {
pub fn new() -> Self {
Self {
cookies: Vec::new(),
}
}
pub fn add(&mut self, c: Cookie) {
let mut i = 0;
while i < self.cookies.len() {
let a = &self.cookies[i];
if a.is_same_key(&c) {
self.cookies[i] = c;
return;
}
i += 1;
}
self.cookies.push(c);
}
/// Check and remove all expired cookies
pub fn check_expired(&mut self) {
let mut i = 0;
while i < self.cookies.len() {
let c = &self.cookies[i];
if c.is_expired() {
self.cookies.remove(i);
} else {
i += 1;
}
}
}
pub fn read(&mut self, file_name: &str) -> bool {
self.cookies.clear();
let p = Path::new(file_name);
if !p.exists() {
println!("{} {}", gettext("Can not find file:"), file_name);
return false;
}
let re = File::open(p);
if re.is_err() {
println!("{} {}", gettext("Can not open file:"), file_name);
return false;
}
let f = re.unwrap();
let r = BufReader::new(f);
for line in r.lines() {
let mut l = line.unwrap();
l = l.trim().to_string();
if l.starts_with("#") {
continue;
}
let mut s = l.split('\t');
if s.clone().count() < 7 {
println!("{} {}", gettext("Invalid cookie:"), l);
return false;
}
let domain = s.next().unwrap();
let subdomains = s.next().unwrap() != "FALSE";
let path = s.next().unwrap();
let http_only = s.next().unwrap() != "FALSE";
let expired = s.next().unwrap();
let name = s.next().unwrap();
let value = s.next().unwrap();
let tmp = expired.trim().parse::<i64>();
if tmp.is_err() {
println!("{} {}", gettext("Can not parse expired time:"), expired);
return false;
}
let tmp = tmp.unwrap();
let expired = if tmp == 0 {
None
} else {
Some(Utc.timestamp(tmp, 0))
};
let c = Cookie::new(name, value, domain, subdomains, path, http_only, expired);
self.add(c);
}
self.check_expired();
true
}
pub fn save(&mut self, file_name: &str) -> bool {
let p = Path::new(file_name);
self.check_expired();
if p.exists() {
let re = remove_file(p);
if re.is_err() {
println!("{} {}", gettext("Failed to remove file:"), re.unwrap_err());
return false;
}
}
let re = File::create(p);
if re.is_err() {
println!("{} {}", gettext("Failed to create file:"), re.unwrap_err());
return false;
}
let mut f = re.unwrap();
for c in self.cookies.iter() {
let r = write!(f, "{}", c.to_netscape_str().as_str());
if r.is_err() {
println!("{} {}", gettext("Failed to write file:"), r.unwrap_err());
return false;
}
}
true
}
pub fn iter(&self) -> core::slice::Iter<Cookie> {
self.cookies.iter()
}
}

15
src/download.rs Normal file
View File

@@ -0,0 +1,15 @@
use crate::gettext;
use crate::pixiv::PixivWebClient;
use crate::Main;
impl Main {
pub fn download(&mut self) -> i32 {
let mut pw = PixivWebClient::new(&self);
if !pw.init() {
println!("{}", gettext("Failed to initialize pixiv web api client."));
return 1;
}
pw.check_login();
0
}
}

167
src/i18n.rs Normal file
View File

@@ -0,0 +1,167 @@
#[cfg(windows)]
extern crate winapi;
use crate::utils::get_exe_path_else_current;
use gettext::Catalog;
use std::fs::File;
pub fn get_lang() -> String {
let lan = std::env::var("LANG");
match lan {
Ok(l) => {
if l.len() > 0 {
return l;
}
}
Err(_) => {}
}
#[cfg(windows)]
{
use std::alloc::alloc;
use std::alloc::dealloc;
use std::alloc::Layout;
use std::mem::align_of;
use std::mem::size_of;
use std::ptr::null_mut;
use winapi::um::stringapiset::WideCharToMultiByte;
use winapi::um::winnls::GetUserDefaultLCID;
use winapi::um::winnls::LCIDToLocaleName;
use winapi::um::winnls::CP_UTF8;
use winapi::um::winnls::WC_ERR_INVALID_CHARS;
use winapi::um::winnt::LPSTR;
use winapi::um::winnt::LPWSTR;
use winapi::um::winnt::WCHAR;
unsafe {
let lcid = GetUserDefaultLCID();
let len = LCIDToLocaleName(lcid, null_mut(), 0, 0);
if len > 0 {
let align = align_of::<WCHAR>();
let s = size_of::<WCHAR>();
let layout = Layout::from_size_align(len as usize * s, align);
match layout {
Ok(lay) => {
let pstr = alloc(lay) as LPWSTR;
let re = LCIDToLocaleName(lcid, pstr, len, 0);
let mut result = String::from("");
if re > 0 {
let mlen = WideCharToMultiByte(
CP_UTF8,
WC_ERR_INVALID_CHARS,
pstr,
len,
null_mut(),
0,
null_mut(),
null_mut(),
);
if mlen > 0 {
let ali = align_of::<u8>();
let layout = Layout::from_size_align(mlen as usize, ali);
match layout {
Ok(lay) => {
let pmstr = alloc(lay) as LPSTR;
let re = WideCharToMultiByte(
CP_UTF8,
WC_ERR_INVALID_CHARS,
pstr,
len,
pmstr,
mlen,
null_mut(),
null_mut(),
);
if re > 0 {
result = String::from_raw_parts(
pmstr as *mut u8,
mlen as usize,
mlen as usize,
);
} else {
dealloc(pmstr as *mut u8, lay);
}
}
Err(_) => {}
}
}
}
dealloc(pstr as *mut u8, lay);
if result.len() > 0 {
return result;
}
}
Err(_) => {}
}
}
}
}
return String::from("en-US");
}
pub struct I18n {
catalog: Option<Catalog>,
}
fn open_mo_file(molang: &str) -> Option<File> {
let mut pb = get_exe_path_else_current();
let base = String::from("pixiv_downloader");
let fname = base + "." + molang.replace("-", "_").as_str() + ".mo";
pb.push(fname);
let p = pb.as_path();
if p.exists() {
let f = File::open(p);
match f {
Ok(f) => {
return Some(f);
}
Err(_) => {}
}
}
return None;
}
impl I18n {
pub fn new() -> I18n {
let s = get_lang();
let mut molang = s.as_str();
if s.starts_with("zh") {
let t = s.to_lowercase();
if t == "zh-tw" || t == "zh-hant" || t == "zh-hk" {
molang = "zh_TW";
} else {
molang = "zh_CN";
}
}
let mut catalog: Option<Catalog> = None;
let re = open_mo_file(molang);
match re {
Some(f) => {
let re = Catalog::parse(f);
match re {
Ok(c) => {
catalog = Some(c);
}
Err(_) => {}
}
}
None => {}
}
return I18n { catalog: catalog };
}
}
lazy_static! {
static ref I18NT: I18n = I18n::new();
}
/// Get translation of text
/// * `s` - Origin text
pub fn gettext(s: &str) -> &str {
match &I18NT.catalog {
Some(c) => {
return c.gettext(s);
}
None => {
return s;
}
}
}

111
src/main.rs Normal file
View File

@@ -0,0 +1,111 @@
extern crate chrono;
extern crate dateparser;
extern crate json;
#[macro_use]
extern crate lazy_static;
extern crate tokio;
extern crate reqwest;
mod cookies;
mod download;
mod i18n;
mod opthelper;
mod opts;
mod parser;
mod pixiv;
mod settings;
mod settings_list;
mod utils;
mod webclient;
use i18n::gettext;
use opts::Command;
use opts::CommandOpts;
use opts::ConfigCommand;
use settings::SettingStore;
pub struct Main {
pub cmd: Option<CommandOpts>,
pub settings: Option<SettingStore>,
}
impl Main {
pub fn deal_config_cmd(&mut self) -> i32 {
let cmd = self.cmd.as_ref().unwrap();
let subcmd = cmd.config_cmd.as_ref().unwrap();
match subcmd {
ConfigCommand::Fix => {
let s = self.settings.as_ref().unwrap();
let conf = cmd.config();
if conf.is_some() {
if s.save(conf.as_ref().unwrap()) {
0
} else {
println!(
"{} {}",
gettext("Failed to save config file:"),
conf.as_ref().unwrap()
);
1
}
} else {
0
}
}
ConfigCommand::Help => {
let s = self.settings.as_ref().unwrap();
println!("{}", gettext("All available settings:"));
s.basic.print_help();
0
}
}
}
pub fn new() -> Self {
Self {
cmd: None,
settings: None,
}
}
pub fn run(&mut self) -> i32 {
self.cmd = opts::parse_cmd();
if self.cmd.is_none() {
return 0;
}
let cmd = self.cmd.as_ref().unwrap();
self.settings = Some(SettingStore::default());
match cmd.config() {
Some(conf) => {
let fix_invalid = if cmd.cmd == Command::Config
&& cmd.config_cmd.as_ref().unwrap() == ConfigCommand::Fix
{
true
} else {
false
};
let r = self.settings.as_mut().unwrap().read(&conf, fix_invalid);
if !r {
println!("{} {}", gettext("Can not read config file:"), conf.as_str());
return 1;
}
}
None => {}
}
match cmd.cmd {
Command::Config => {
self.deal_config_cmd();
}
Command::Download => {
return self.download();
}
}
0
}
}
#[tokio::main]
async fn main() {
let mut m = Main::new();
std::process::exit(m.run());
}

34
src/opthelper.rs Normal file
View File

@@ -0,0 +1,34 @@
use crate::opts::CommandOpts;
use crate::settings::SettingStore;
#[derive(Clone)]
pub struct OptHelper<'a> {
opt: &'a CommandOpts,
settings: &'a SettingStore,
}
impl<'a> OptHelper<'a> {
pub fn cookies(&self) -> Option<String> {
if self.opt.cookies.is_some() {
self.opt.cookies.clone()
} else if self.settings.have_str("cookies") {
self.settings.get_str("cookies")
} else {
None
}
}
pub fn language(&self) -> Option<String> {
if self.opt.language.is_some() {
self.opt.language.clone()
} else if self.settings.have_str("language") {
self.settings.get_str("language")
} else {
None
}
}
pub fn new(opt: &'a CommandOpts, settings: &'a SettingStore) -> Self {
Self { opt, settings }
}
}

193
src/opts.rs Normal file
View File

@@ -0,0 +1,193 @@
extern crate getopts;
use crate::gettext;
use crate::utils::check_file_exists;
use crate::utils::get_exe_path_else_current;
use getopts::Options;
use std::env;
/// Command Line command
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum Command {
/// Do something for the config
Config,
/// Download an artwork
Download,
}
/// Subcommand for config
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum ConfigCommand {
/// Fix the config file
Fix,
/// Print all available settings
Help,
}
impl PartialEq<ConfigCommand> for &ConfigCommand {
fn eq(&self, other: &ConfigCommand) -> bool {
other == *self
}
}
#[derive(Debug)]
/// Command Line Options
pub struct CommandOpts {
/// Command
pub cmd: Command,
/// URLs
pub urls: Vec<String>,
/// Config location
pub _config: Option<String>,
/// Config command
pub config_cmd: Option<ConfigCommand>,
/// The location of cookies file
pub cookies: Option<String>,
/// The language of translated tags
pub language: Option<String>,
}
impl CommandOpts {
pub fn new(cmd: Command) -> Self {
Self {
cmd,
urls: Vec::new(),
_config: None,
config_cmd: None,
cookies: None,
language: None,
}
}
pub fn config(&self) -> Option<String> {
if self._config.is_some() {
if check_file_exists(&self._config.as_ref().unwrap()) {
self._config.clone()
} else {
println!(
"{}",
gettext("Warning: The specified config file not found.")
);
None
}
} else {
let mut pb = get_exe_path_else_current();
pb = pb.join("pixiv_downloader.json");
if pb.exists() {
return Some(String::from(pb.to_str().unwrap()));
}
if check_file_exists("config.json") {
return Some(String::from("config.json"));
}
None
}
}
}
pub fn print_usage(prog: &str, opts: &Options) {
let brief = format!(
"{}
{} download [options] <url> [<url>] {}
{} config fix [options] {}
{} config help [options] {}",
gettext("Usage:"),
prog,
gettext("Download an artwork"),
prog,
gettext("Fix the config file"),
prog,
gettext("Print all available settings"),
);
println!("{}", opts.usage(brief.as_str()));
}
pub fn parse_cmd() -> Option<CommandOpts> {
let argv: Vec<String> = env::args().collect();
let mut opts = Options::new();
opts.optflag("h", "help", gettext("Print help message."));
opts.optopt(
"c",
"config",
gettext("The location of config file."),
"FILE",
);
opts.optopt(
"C",
"cookies",
gettext("The location of cookies file. Used for web API."),
"FILE",
);
opts.optopt(
"l",
"language",
gettext("The language of translated tags."),
"LANG",
);
let result = match opts.parse(&argv[1..]) {
Ok(m) => m,
Err(err) => {
panic!("{}", err.to_string())
}
};
if result.opt_present("h") || result.free.len() == 0 {
print_usage(&argv[0], &opts);
return None;
}
let cmd = &result.free[0];
let mut re = if cmd == "download" {
Some(CommandOpts::new(Command::Download))
} else if cmd == "config" {
Some(CommandOpts::new(Command::Config))
} else {
None
};
if re.is_none() {
println!("{}", gettext("Unknown command."));
print_usage(&argv[0], &opts);
return None;
}
match re.as_ref().unwrap().cmd {
Command::Download => {
let mut urls = Vec::new();
for url in result.free.iter().skip(1) {
urls.push(url.to_string());
}
if urls.is_empty() {
println!("{}", gettext("No URL specified."));
print_usage(&argv[0], &opts);
return None;
}
re.as_mut().unwrap().urls = urls;
}
Command::Config => {
if result.free.len() < 2 {
println!("{}", gettext("No detailed command specified."));
print_usage(&argv[0], &opts);
return None;
}
let subcmd = &result.free[1];
re.as_mut().unwrap().config_cmd = if subcmd == "fix" {
Some(ConfigCommand::Fix)
} else if subcmd == "help" {
Some(ConfigCommand::Help)
} else {
None
};
if re.as_ref().unwrap().config_cmd.is_none() {
println!("{}", gettext("Unknown config subcommand."));
print_usage(&argv[0], &opts);
return None;
}
}
}
if result.opt_present("config") {
re.as_mut().unwrap()._config = Some(result.opt_str("config").unwrap());
}
if result.opt_present("cookies") {
re.as_mut().unwrap().cookies = Some(result.opt_str("cookies").unwrap());
}
if result.opt_present("language") {
re.as_mut().unwrap().language = Some(result.opt_str("language").unwrap());
}
re
}

87
src/parser/mainpage.rs Normal file
View File

@@ -0,0 +1,87 @@
use crate::gettext;
use html_parser::Dom;
use html_parser::Node;
use json::JsonValue;
pub struct MainPageParser {
pub value: Option<JsonValue>,
}
impl MainPageParser {
pub fn new() -> Self {
Self {
value: None,
}
}
fn iter(&mut self, node: &Node) -> bool {
match node {
Node::Element(e) => {
if e.name == "meta" {
let name = e.attributes.get("name");
if name.is_none() {
return false;
}
let name = name.unwrap();
if name.is_none() {
return false;
}
if name.as_ref().unwrap() != "global-data" {
return false;
}
if e.id.is_none() {
return false;
}
if e.id.as_ref().unwrap() != "meta-global-data" {
return false;
}
let c = e.attributes.get("content");
if c.is_none() {
return false;
}
let c = c.unwrap();
if c.is_none() {
return false;
}
let r = json::parse(c.as_ref().unwrap());
if r.is_err() {
println!("{} {}", gettext("Failed to parse JSON:"), r.unwrap_err());
return false;
}
self.value = Some(r.unwrap());
true
} else {
for c in e.children.iter() {
if self.iter(c) {
return true;
}
}
false
}
}
Node::Comment(_) => { false }
Node::Text(_) => { false }
}
}
pub fn parse(&mut self, context: &str) -> bool {
let r = Dom::parse(context);
if r.is_err() {
println!("{} {}", gettext("Failed to parse HTML:"), r.unwrap_err());
return false;
}
let dom = r.unwrap();
if dom.errors.len() > 0 {
println!("{}", gettext("Some errors occured during parsing:"));
for i in dom.errors.iter() {
println!("{}", i);
}
}
for n in dom.children.iter() {
if self.iter(n) {
return true;
}
}
false
}
}

3
src/parser/mod.rs Normal file
View File

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

91
src/pixiv.rs Normal file
View File

@@ -0,0 +1,91 @@
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());
}
}
}
}
}

421
src/settings.rs Normal file
View File

@@ -0,0 +1,421 @@
use crate::gettext;
use crate::settings_list::get_settings_list;
use json::JsonValue;
use std::collections::HashMap;
use std::fmt::{Debug, Display};
use std::fs::{remove_file, File};
use std::io::{Read, Write};
use std::path::Path;
/// Json value type
#[derive(Clone, Copy, PartialEq)]
pub enum JsonValueType {
Str,
Number,
Boolean,
Object,
Array,
Multiple,
}
impl JsonValueType {
pub fn to_str(&self) -> &'static str {
match self {
JsonValueType::Str => "String",
JsonValueType::Number => "Number",
JsonValueType::Boolean => "Boolean",
JsonValueType::Object => "Object",
JsonValueType::Array => "Array",
JsonValueType::Multiple => gettext("Multiple type"),
}
}
}
impl Debug for JsonValueType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.to_str())
}
}
impl Display for JsonValueType {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(f, "{}", self.to_str())
}
}
/// An callback to check if a json value is valid
pub type SettingDesCallback = fn(obj: &JsonValue) -> bool;
/// An object to describe a setting
#[derive(Clone)]
pub struct SettingDes {
/// The name of the setting
_name: String,
/// The description of the setting
_description: String,
/// The type of the setting
_type: JsonValueType,
/// The callback function of the setting
_fun: Option<SettingDesCallback>,
}
impl SettingDes {
/// Create a new setting description
pub fn new(
name: &str,
description: &str,
typ: JsonValueType,
callback: Option<SettingDesCallback>,
) -> Option<SettingDes> {
if (typ == JsonValueType::Array
|| typ == JsonValueType::Object
|| typ == JsonValueType::Multiple)
&& callback.is_none()
{
return None;
}
Some(SettingDes {
_name: String::from(name),
_description: String::from(description),
_type: typ.clone(),
_fun: callback,
})
}
pub fn name(&self) -> &str {
self._name.as_str()
}
pub fn description(&self) -> &str {
self._description.as_str()
}
pub fn type_name(&self) -> &'static str {
self._type.to_str()
}
/// Check if a value is valid
pub fn is_vaild_value(&self, value: &JsonValue) -> bool {
if self._type == JsonValueType::Array {
if value.is_array() {
return self._fun.unwrap()(&value);
}
} else if self._type == JsonValueType::Boolean {
if value.is_boolean() {
return true;
}
} else if self._type == JsonValueType::Multiple {
return self._fun.unwrap()(&value);
} else if self._type == JsonValueType::Number {
if value.is_number() {
match self._fun {
Some(fun) => {
return fun(&value);
}
None => {
return true;
}
}
}
} else if self._type == JsonValueType::Object {
if value.is_object() {
return self._fun.unwrap()(&value);
}
} else if self._type == JsonValueType::Str {
if value.is_string() {
match self._fun {
Some(fun) => {
return fun(&value);
}
None => {
return true;
}
}
}
}
return false;
}
}
impl Debug for SettingDes {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
write!(
f,
"SettingDes {{ name: {}, description: {}, type: {} }}",
self._name, self._description, self._type
)
}
}
/// Store a list of settings
#[derive(Clone, Debug)]
pub struct SettingDesStore {
list: Vec<SettingDes>,
}
impl SettingDesStore {
pub fn new(list: Vec<SettingDes>) -> SettingDesStore {
SettingDesStore { list }
}
pub fn check_valid(&self, key: &str, value: &JsonValue) -> Option<bool> {
for i in self.list.iter() {
if i.name() == key {
return Some(i.is_vaild_value(value));
}
}
None
}
pub fn len(&self) -> usize {
self.list.len()
}
pub fn print_help(&self) {
let mut s = String::from("");
for i in self.list.iter() {
let mut t = format!("{}: {}", i.name(), i.type_name());
if t.len() >= 20 {
t += "\t";
} else {
t += " ".repeat(20 - t.len()).as_str();
}
t += i.description();
if s.len() > 0 {
s += "\n";
}
s += t.as_str();
}
println!("{}", s);
}
}
impl Default for SettingDesStore {
fn default() -> Self {
Self {
list: get_settings_list(),
}
}
}
/// An settings option
#[derive(Clone)]
pub struct SettingOpt {
_name: String,
_value: JsonValue,
}
impl SettingOpt {
pub fn new(name: String, value: JsonValue) -> SettingOpt {
SettingOpt {
_name: name.clone(),
_value: value.clone(),
}
}
pub fn name(&self) -> String {
self._name.clone()
}
pub fn value(&self) -> JsonValue {
self._value.clone()
}
}
impl Debug for SettingOpt {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
Display::fmt(&self._value, f)
}
}
#[derive(Clone, Debug)]
pub struct SettingJar {
pub settings: HashMap<String, SettingOpt>,
}
impl SettingJar {
pub fn new() -> SettingJar {
SettingJar {
settings: HashMap::new(),
}
}
pub fn add(&mut self, key: &str, opt: JsonValue) {
self.settings
.insert(String::from(key), SettingOpt::new(String::from(key), opt));
}
pub fn clear(&mut self) {
self.settings.clear();
}
pub fn get(&self, key: &str) -> Option<JsonValue> {
if self.settings.contains_key(key) {
return Some(self.settings.get(key).unwrap().value());
}
None
}
pub fn to_json(&self) -> Option<JsonValue> {
let mut v = JsonValue::new_object();
for (_, val) in self.settings.iter() {
match v.insert(val.name().as_str(), val.value()) {
Ok(_) => {}
Err(_) => {
println!("{}", gettext("Can not insert setting to JSON object."));
return None;
}
}
}
Some(v)
}
}
#[derive(Clone, Debug)]
pub struct SettingStore {
pub basic: SettingDesStore,
pub data: SettingJar,
}
impl SettingStore {
pub fn new(list: Vec<SettingDes>) -> Self {
Self {
basic: SettingDesStore::new(list),
data: SettingJar::new(),
}
}
pub fn get_str(&self, key: &str) -> Option<String> {
let obj = self.data.get(key);
if obj.is_none() {
return None;
}
let obj = obj.unwrap();
if !obj.is_string() {
None
} else {
Some(String::from(obj.as_str().unwrap()))
}
}
pub fn have_str(&self, key: &str) -> bool {
let obj = self.data.get(key);
if obj.is_none() {
return false;
}
let obj = obj.unwrap();
obj.is_string()
}
pub fn read(&mut self, file_name: &str, fix_invalid: bool) -> bool {
self.data.clear();
let path = Path::new(file_name);
if !path.exists() {
return false;
}
let re = File::open(path);
if re.is_err() {
println!("{}", re.unwrap_err());
return false;
}
let mut f = re.unwrap();
let mut s = String::from("");
let r = f.read_to_string(&mut s);
match r {
Ok(le) => {
if le == 0 {
if !fix_invalid {
println!("{}", gettext("Settings file is empty."));
return false;
}
return true;
}
}
Err(_) => {
println!("{}", gettext("Can not read from settings file."));
return false;
}
}
let re = json::parse(s.as_str());
match re {
Ok(_) => {}
Err(_) => {
if !fix_invalid {
println!("{}", gettext("Can not parse settings file."));
return false;
}
return true;
}
}
let obj = re.unwrap();
if !obj.is_object() {
if !fix_invalid {
println!("{}", gettext("Unknown settings file."));
return false;
}
return true;
}
for (key, o) in obj.entries() {
let re = self.basic.check_valid(key, o);
match re {
Some(re) => {
if !re {
if !fix_invalid {
let s = gettext("\"<key>\" is invalid, you can use \"pixiv_downloader config fix\" to remove all invalid value.").replace("<key>", key);
println!("{}", s.as_str());
return false;
}
} else {
self.data.add(key, o.clone());
}
}
None => {
self.data.add(key, o.clone());
}
}
}
true
}
pub fn save(&self, file_name: &str) -> bool {
let obj = self.data.to_json();
if obj.is_none() {
println!("{}", gettext("Can not convert settings to JSON object."));
return false;
}
let obj = obj.unwrap();
let s = json::stringify(obj);
let path = Path::new(file_name);
if path.exists() {
match remove_file(path) {
Ok(_) => {}
Err(e) => {
println!("{} {}", gettext("Failed to remove file:"), e);
return false;
}
}
}
let r = File::create(path);
if r.is_err() {
println!("{} {}", gettext("Failed to create file:"), r.unwrap_err());
return false;
}
let mut f = r.unwrap();
let r = f.write(s.as_bytes());
if r.is_err() {
println!("{} {}", gettext("Failed to write file:"), r.unwrap_err());
return false;
}
let r = f.flush();
if r.is_err() {
println!("{} {}", gettext("Failed to flush file:"), r.unwrap_err());
return false;
}
true
}
}
impl Default for SettingStore {
fn default() -> Self {
Self::new(get_settings_list())
}
}

11
src/settings_list.rs Normal file
View File

@@ -0,0 +1,11 @@
use crate::gettext;
use crate::settings::SettingDes;
use crate::settings::JsonValueType;
pub fn get_settings_list() -> Vec<SettingDes> {
vec![
SettingDes::new("refresh_tokens", gettext("Pixiv's refresh tokens. Used to login."), JsonValueType::Str, None).unwrap(),
SettingDes::new("cookies", gettext("The location of cookies file. Used for web API."), JsonValueType::Str, None).unwrap(),
SettingDes::new("language", gettext("The language of translated tags."), JsonValueType::Str, None).unwrap(),
]
}

24
src/utils.rs Normal file
View File

@@ -0,0 +1,24 @@
use std::env;
use std::path::Path;
use std::path::PathBuf;
/// Get executable location, if not found, return current directory (./)
pub fn get_exe_path_else_current() -> PathBuf {
let re = env::current_exe();
match re {
Ok(pa) => {
let mut p = pa.clone();
p.pop();
p
}
Err(_) => {
let p = Path::new("./");
p.to_path_buf()
}
}
}
pub fn check_file_exists(path: &str) -> bool {
let p = Path::new(path);
p.exists()
}

115
src/webclient.rs Normal file
View File

@@ -0,0 +1,115 @@
extern crate spin_on;
use crate::cookies::Cookie;
use crate::cookies::CookieJar;
use crate::gettext;
use reqwest::{Client, IntoUrl, RequestBuilder, Response};
use std::collections::HashMap;
use spin_on::spin_on;
/// Generate `cookie` header for a url
/// * `c` - Cookies
/// * `url` - URL
pub fn gen_cookie_header<U: IntoUrl>(c: &mut CookieJar, url: U) -> String {
c.check_expired();
let mut s = String::from("");
let u = url.as_str();
for a in c.iter() {
if a.matched(u) {
if s.len() > 0 {
s += " ";
}
s += a.get_name_value().as_str();
}
}
s
}
/// A Web Client
pub struct WebClient {
client: Client,
/// HTTP Headers
pub headers: HashMap<String, String>,
cookies: CookieJar,
}
impl WebClient {
pub fn new() -> Self {
Self {
client: Client::new(),
headers: HashMap::new(),
cookies: CookieJar::new(),
}
}
pub fn handle_set_cookie(&mut self, r: &Response) {
let u = r.url();
let h = r.headers();
let v = h.get_all("Set-Cookie");
for val in v {
let val = val.to_str();
match val {
Ok(val) => {
let c = Cookie::from_set_cookie(u.as_str(), val);
match c {
Some(c) => {
self.cookies.add(c);
}
None => {
println!("{}", gettext("Failed to parse Set-Cookie header."));
}
}
}
Err(e) => {
println!("{} {}", gettext("Failed to convert to string:"), e);
}
}
}
}
pub fn read_cookies(&mut self, file_name: &str) -> bool {
let r = self.cookies.read(file_name);
if !r {
self.cookies = CookieJar::new();
}
r
}
pub fn save_cookies(&mut self, file_name: &str) -> bool {
self.cookies.save(file_name)
}
pub fn set_header(&mut self, key: &str, value: &str) -> Option<String> {
self.headers.insert(String::from(key), String::from(value))
}
/// Send GET requests
pub fn get<U: IntoUrl>(&mut self, url: U) -> Option<Response> {
let r = self.aget(url);
let r = r.send();
let r = spin_on(r);
match r {
Ok(_) => {}
Err(e) => {
println!("{} {}", gettext("Error when request:"), e);
return None;
}
}
let r = r.unwrap();
self.handle_set_cookie(&r);
Some(r)
}
pub fn aget<U: IntoUrl>(&mut self, url: U) -> RequestBuilder {
let s = url.as_str();
let mut r = self.client.get(s);
for (k, v) in self.headers.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
}
}