mirror of
https://github.com/lifegpc/pixiv_downloader.git
synced 2026-07-08 01:32:41 +08:00
Add file
This commit is contained in:
389
src/cookies.rs
Normal file
389
src/cookies.rs
Normal 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
15
src/download.rs
Normal 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
167
src/i18n.rs
Normal 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
111
src/main.rs
Normal 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
34
src/opthelper.rs
Normal 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
193
src/opts.rs
Normal 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
87
src/parser/mainpage.rs
Normal 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
3
src/parser/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
extern crate html_parser;
|
||||
|
||||
pub mod mainpage;
|
||||
91
src/pixiv.rs
Normal file
91
src/pixiv.rs
Normal 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
421
src/settings.rs
Normal 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
11
src/settings_list.rs
Normal 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
24
src/utils.rs
Normal 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
115
src/webclient.rs
Normal 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user