diff --git a/Language/pixiv_downloader.pot b/Language/pixiv_downloader.pot index 2df24bc..3c03cd3 100644 --- a/Language/pixiv_downloader.pot +++ b/Language/pixiv_downloader.pot @@ -2,7 +2,7 @@ msgid "" msgstr "" "Project-Id-Version: pixiv_downloader\n" -"POT-Creation-Date: 2022-03-08 18:02+0800\n" +"POT-Creation-Date: 2022-03-09 22:44+0800\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -69,15 +69,15 @@ msgstr "" msgid "Can not parse expired time:" msgstr "" -#: cookies.rs:366 data/json.rs:53 settings.rs:396 webclient.rs:275 +#: cookies.rs:366 data/json.rs:53 settings.rs:404 webclient.rs:288 msgid "Failed to remove file:" msgstr "" -#: cookies.rs:372 data/json.rs:64 settings.rs:403 webclient.rs:281 +#: cookies.rs:372 data/json.rs:64 settings.rs:411 webclient.rs:294 msgid "Failed to create file:" msgstr "" -#: cookies.rs:379 data/json.rs:70 settings.rs:409 webclient.rs:294 +#: cookies.rs:379 data/json.rs:70 settings.rs:417 webclient.rs:307 msgid "Failed to write file:" msgstr "" @@ -85,7 +85,7 @@ msgstr "" msgid "Failed to unescape string:" msgstr "" -#: download.rs:18 pixiv_web.rs:57 +#: download.rs:18 pixiv_web.rs:58 msgid "Failed to initialize pixiv web api client." msgstr "" @@ -114,7 +114,7 @@ msgid "Failed to get file name from url:" msgstr "" #: download.rs:90 download.rs:96 download.rs:133 download.rs:139 -#: pixiv_web.rs:152 +#: pixiv_web.rs:153 msgid "Failed to download image:" msgstr "" @@ -126,82 +126,94 @@ msgstr "" msgid "Failed to add exif data to image:" msgstr "" -#: opts.rs:79 +#: dur.rs:73 +msgid "Failed to parse duration from string." +msgstr "" + +#: opts.rs:85 msgid "Warning: The specified config file not found." msgstr "" -#: opts.rs:103 +#: opts.rs:109 msgid "Usage:" msgstr "" -#: opts.rs:105 +#: opts.rs:111 msgid "Download an artwork" msgstr "" -#: opts.rs:107 +#: opts.rs:113 msgid "Fix the config file" msgstr "" -#: opts.rs:109 +#: opts.rs:115 msgid "Print all available settings" msgstr "" -#: opts.rs:117 +#: opts.rs:123 msgid "Print help message." msgstr "" -#: opts.rs:121 +#: opts.rs:127 msgid "The location of config file." msgstr "" -#: opts.rs:127 settings_list.rs:8 +#: opts.rs:133 settings_list.rs:10 msgid "The location of cookies file. Used for web API." msgstr "" -#: opts.rs:133 settings_list.rs:9 +#: opts.rs:139 settings_list.rs:11 msgid "The language of translated tags." msgstr "" -#: opts.rs:136 +#: opts.rs:142 msgid "Verbose logging." msgstr "" -#: opts.rs:137 +#: opts.rs:143 msgid "Overwrite existing file." msgstr "" -#: opts.rs:138 +#: opts.rs:144 msgid "Skip overwrite existing file." msgstr "" -#: opts.rs:142 settings_list.rs:10 +#: opts.rs:148 settings_list.rs:12 msgid "Max retry count if request failed." msgstr "" -#: opts.rs:164 +#: opts.rs:154 settings_list.rs:13 +msgid "The interval (in seconds) between two retries." +msgstr "" + +#: opts.rs:176 msgid "Unknown command." msgstr "" -#: opts.rs:174 +#: opts.rs:186 msgid "Failed to parse ID:" msgstr "" -#: opts.rs:180 +#: opts.rs:192 msgid "No URL or ID specified." msgstr "" -#: opts.rs:188 +#: opts.rs:200 msgid "No detailed command specified." msgstr "" -#: opts.rs:201 +#: opts.rs:213 msgid "Unknown config subcommand." msgstr "" -#: opts.rs:237 +#: opts.rs:249 msgid "Retry count must be an non-negative integer:" msgstr "" +#: opts.rs:258 +msgid "Failed to parse retry interval:" +msgstr "" + #: parser/description.rs:129 parser/metadata.rs:76 msgid "Failed to parse HTML:" msgstr "" @@ -214,87 +226,99 @@ msgstr "" msgid "There are some nodes still in stack:" msgstr "" -#: parser/metadata.rs:54 pixiv_web.rs:111 +#: parser/metadata.rs:54 pixiv_web.rs:112 msgid "Failed to parse JSON:" msgstr "" -#: pixiv_web.rs:72 pixiv_web.rs:77 +#: pixiv_web.rs:73 pixiv_web.rs:78 msgid "Failed to get main page:" msgstr "" -#: pixiv_web.rs:83 +#: pixiv_web.rs:84 msgid "Failed to parse main page." msgstr "" -#: pixiv_web.rs:87 +#: pixiv_web.rs:88 msgid "Main page's data:" msgstr "" -#: pixiv_web.rs:102 +#: pixiv_web.rs:103 msgid "Network error:" msgstr "" -#: pixiv_web.rs:121 +#: pixiv_web.rs:122 msgid "Failed to detect error." msgstr "" -#: pixiv_web.rs:168 pixiv_web.rs:173 +#: pixiv_web.rs:169 pixiv_web.rs:174 msgid "Failed to get artwork page:" msgstr "" -#: pixiv_web.rs:179 +#: pixiv_web.rs:180 msgid "Failed to parse artwork page." msgstr "" -#: pixiv_web.rs:183 +#: pixiv_web.rs:184 msgid "Artwork's data:" msgstr "" -#: pixiv_web.rs:197 +#: pixiv_web.rs:198 msgid "Artwork's page data:" msgstr "" -#: pixiv_web.rs:221 +#: pixiv_web.rs:222 msgid "Warning: Failed to save cookies file:" msgstr "" +#: retry_interval.rs:22 +msgid "Failed to get JSON object." +msgstr "" + +#: retry_interval.rs:37 retry_interval.rs:55 +msgid "Failed to parse JSON number." +msgstr "" + +#: retry_interval.rs:61 retry_interval.rs:65 +msgid "Unsupported JSON type." +msgstr "" + #: settings.rs:29 msgid "Multiple type" msgstr "" -#: settings.rs:264 +#: settings.rs:268 msgid "Can not insert setting to JSON object." msgstr "" -#: settings.rs:331 +#: settings.rs:339 msgid "Settings file is empty." msgstr "" -#: settings.rs:338 +#: settings.rs:346 msgid "Can not read from settings file." msgstr "" -#: settings.rs:347 +#: settings.rs:355 msgid "Can not parse settings file." msgstr "" -#: settings.rs:356 +#: settings.rs:364 msgid "Unknown settings file." msgstr "" -#: settings.rs:367 +#: settings.rs:375 msgid "\"\" is invalid, you can use \"pixiv_downloader config fix\" to remove all invalid value." msgstr "" -#: settings.rs:386 +#: settings.rs:394 msgid "Can not convert settings to JSON object." msgstr "" -#: settings.rs:414 +#: settings.rs:422 msgid "Failed to flush file:" msgstr "" -#: settings_list.rs:7 +#: settings_list.rs:9 msgid "Pixiv's refresh tokens. Used to login." msgstr "" @@ -302,7 +326,7 @@ msgstr "" msgid "Do you want to delete file \"\"?" msgstr "" -#: utils.rs:51 webclient.rs:146 +#: utils.rs:51 webclient.rs:152 msgid "Can not parse URL:" msgstr "" @@ -310,46 +334,50 @@ msgstr "" msgid "Failed to get file name from path:" msgstr "" -#: webclient.rs:106 +#: webclient.rs:112 msgid "Failed to parse Set-Cookie header." msgstr "" -#: webclient.rs:111 +#: webclient.rs:117 msgid "Failed to convert to string:" msgstr "" -#: webclient.rs:153 +#: webclient.rs:159 msgid "Parameters should be object or array:" msgstr "" -#: webclient.rs:173 +#: webclient.rs:179 msgid "Parameters should be array:" msgstr "" -#: webclient.rs:177 +#: webclient.rs:183 msgid "Parameters need at least a value:" msgstr "" -#: webclient.rs:213 +#: webclient.rs:222 +msgid "Retry after seconds." +msgstr "" + +#: webclient.rs:226 msgid "Retry times now." msgstr "" -#: webclient.rs:227 +#: webclient.rs:240 msgid "Error when request:" msgstr "" -#: webclient.rs:288 +#: webclient.rs:301 msgid "Error when downloading file:" msgstr "" -#: main.rs:65 +#: main.rs:68 msgid "Failed to save config file:" msgstr "" -#: main.rs:76 +#: main.rs:79 msgid "All available settings:" msgstr "" -#: main.rs:108 +#: main.rs:111 msgid "Can not read config file:" msgstr "" diff --git a/Language/pixiv_downloader.zh_CN.po b/Language/pixiv_downloader.zh_CN.po index cdbf316..b9fead0 100644 --- a/Language/pixiv_downloader.zh_CN.po +++ b/Language/pixiv_downloader.zh_CN.po @@ -1,8 +1,8 @@ msgid "" msgstr "" "Project-Id-Version: pixiv_downloader\n" -"POT-Creation-Date: 2022-03-08 18:02+0800\n" -"PO-Revision-Date: 2022-03-08 18:02+0800\n" +"POT-Creation-Date: 2022-03-09 22:44+0800\n" +"PO-Revision-Date: 2022-03-09 22:45+0800\n" "Last-Translator: lifegpc \n" "Language-Team: \n" "Language: zh_CN\n" @@ -70,15 +70,15 @@ msgstr "无效的Cookie:" msgid "Can not parse expired time:" msgstr "无法解析过期时间:" -#: cookies.rs:366 data/json.rs:53 settings.rs:396 webclient.rs:275 +#: cookies.rs:366 data/json.rs:53 settings.rs:404 webclient.rs:288 msgid "Failed to remove file:" msgstr "无法删除文件:" -#: cookies.rs:372 data/json.rs:64 settings.rs:403 webclient.rs:281 +#: cookies.rs:372 data/json.rs:64 settings.rs:411 webclient.rs:294 msgid "Failed to create file:" msgstr "无法创建文件:" -#: cookies.rs:379 data/json.rs:70 settings.rs:409 webclient.rs:294 +#: cookies.rs:379 data/json.rs:70 settings.rs:417 webclient.rs:307 msgid "Failed to write file:" msgstr "无法写入文件:" @@ -86,7 +86,7 @@ msgstr "无法写入文件:" msgid "Failed to unescape string:" msgstr "无法反转义字符串:" -#: download.rs:18 pixiv_web.rs:57 +#: download.rs:18 pixiv_web.rs:58 msgid "Failed to initialize pixiv web api client." msgstr "无法初始化 Pixiv 网页 API 客户端。" @@ -115,7 +115,7 @@ msgid "Failed to get file name from url:" msgstr "无法从 URL 获取文件名:" #: download.rs:90 download.rs:96 download.rs:133 download.rs:139 -#: pixiv_web.rs:152 +#: pixiv_web.rs:153 msgid "Failed to download image:" msgstr "无法下载图片:" @@ -127,82 +127,94 @@ msgstr "已下载图片:" msgid "Failed to add exif data to image:" msgstr "无法往图片增加 EXIF 数据:" -#: opts.rs:79 +#: dur.rs:73 +msgid "Failed to parse duration from string." +msgstr "无法解析从字符串解析时长:" + +#: opts.rs:85 msgid "Warning: The specified config file not found." msgstr "警告:没有找到指定的设置文件。" -#: opts.rs:103 +#: opts.rs:109 msgid "Usage:" msgstr "使用方法:" -#: opts.rs:105 +#: opts.rs:111 msgid "Download an artwork" msgstr "下载一个作品" -#: opts.rs:107 +#: opts.rs:113 msgid "Fix the config file" msgstr "修复设置文件" -#: opts.rs:109 +#: opts.rs:115 msgid "Print all available settings" msgstr "打印所有可用的设置" -#: opts.rs:117 +#: opts.rs:123 msgid "Print help message." msgstr "打印帮助信息。" -#: opts.rs:121 +#: opts.rs:127 msgid "The location of config file." msgstr "设置文件的位置。" -#: opts.rs:127 settings_list.rs:8 +#: opts.rs:133 settings_list.rs:10 msgid "The location of cookies file. Used for web API." msgstr "cookies 文件的位置。用于网页 API。" -#: opts.rs:133 settings_list.rs:9 +#: opts.rs:139 settings_list.rs:11 msgid "The language of translated tags." msgstr "翻译后的标签语言。" -#: opts.rs:136 +#: opts.rs:142 msgid "Verbose logging." msgstr "启用详细内容输出。" -#: opts.rs:137 +#: opts.rs:143 msgid "Overwrite existing file." msgstr "覆盖已有文件。" -#: opts.rs:138 +#: opts.rs:144 msgid "Skip overwrite existing file." msgstr "跳过覆盖已有文件。" -#: opts.rs:142 settings_list.rs:10 +#: opts.rs:148 settings_list.rs:12 msgid "Max retry count if request failed." msgstr "请求失败时最大重试次数。" -#: opts.rs:164 +#: opts.rs:154 settings_list.rs:13 +msgid "The interval (in seconds) between two retries." +msgstr "两次尝试的间隔时间(单位:秒)。" + +#: opts.rs:176 msgid "Unknown command." msgstr "未知指令。" -#: opts.rs:174 +#: opts.rs:186 msgid "Failed to parse ID:" msgstr "无法解析 ID:" -#: opts.rs:180 +#: opts.rs:192 msgid "No URL or ID specified." msgstr "没有指定网址或 ID。" -#: opts.rs:188 +#: opts.rs:200 msgid "No detailed command specified." msgstr "没有指定更详细的指令。" -#: opts.rs:201 +#: opts.rs:213 msgid "Unknown config subcommand." msgstr "未知的 config 子指令。" -#: opts.rs:237 +#: opts.rs:249 msgid "Retry count must be an non-negative integer:" msgstr "重试次数不能是负数:" +#: opts.rs:258 +msgid "Failed to parse retry interval:" +msgstr "无法解析间隔时间:" + #: parser/description.rs:129 parser/metadata.rs:76 msgid "Failed to parse HTML:" msgstr "无法解析 HTML:" @@ -215,75 +227,87 @@ msgstr "在解析中发生了一些错误:" msgid "There are some nodes still in stack:" msgstr "堆栈中依旧有一些节点:" -#: parser/metadata.rs:54 pixiv_web.rs:111 +#: parser/metadata.rs:54 pixiv_web.rs:112 msgid "Failed to parse JSON:" msgstr "无法解析 JSON:" -#: pixiv_web.rs:72 pixiv_web.rs:77 +#: pixiv_web.rs:73 pixiv_web.rs:78 msgid "Failed to get main page:" msgstr "无法获取主页:" -#: pixiv_web.rs:83 +#: pixiv_web.rs:84 msgid "Failed to parse main page." msgstr "无法解析主页。" -#: pixiv_web.rs:87 +#: pixiv_web.rs:88 msgid "Main page's data:" msgstr "主页的数据:" -#: pixiv_web.rs:102 +#: pixiv_web.rs:103 msgid "Network error:" msgstr "网络错误:" -#: pixiv_web.rs:121 +#: pixiv_web.rs:122 msgid "Failed to detect error." msgstr "检测 error 字段失败。" -#: pixiv_web.rs:168 pixiv_web.rs:173 +#: pixiv_web.rs:169 pixiv_web.rs:174 msgid "Failed to get artwork page:" msgstr "无法获取作品页:" -#: pixiv_web.rs:179 +#: pixiv_web.rs:180 msgid "Failed to parse artwork page." msgstr "无法解析作品页。" -#: pixiv_web.rs:183 +#: pixiv_web.rs:184 msgid "Artwork's data:" msgstr "作品数据:" -#: pixiv_web.rs:197 +#: pixiv_web.rs:198 msgid "Artwork's page data:" msgstr "作品页面数据:" -#: pixiv_web.rs:221 +#: pixiv_web.rs:222 msgid "Warning: Failed to save cookies file:" msgstr "警告:无法保存 cookies 文件:" +#: retry_interval.rs:22 +msgid "Failed to get JSON object." +msgstr "无法获取 JSON 对象。" + +#: retry_interval.rs:37 retry_interval.rs:55 +msgid "Failed to parse JSON number." +msgstr "无法解析 JSON 数字:" + +#: retry_interval.rs:61 retry_interval.rs:65 +msgid "Unsupported JSON type." +msgstr "不支持的 JSON 类型。" + #: settings.rs:29 msgid "Multiple type" msgstr "多种类型" -#: settings.rs:264 +#: settings.rs:268 msgid "Can not insert setting to JSON object." msgstr "无法将设置插入 JSON 对象。" -#: settings.rs:331 +#: settings.rs:339 msgid "Settings file is empty." msgstr "设置文件是空的。" -#: settings.rs:338 +#: settings.rs:346 msgid "Can not read from settings file." msgstr "无法读取设置文件。" -#: settings.rs:347 +#: settings.rs:355 msgid "Can not parse settings file." msgstr "无法解析设置文件。" -#: settings.rs:356 +#: settings.rs:364 msgid "Unknown settings file." msgstr "未知的设置文件。" -#: settings.rs:367 +#: settings.rs:375 msgid "" "\"\" is invalid, you can use \"pixiv_downloader config fix\" to remove " "all invalid value." @@ -291,15 +315,15 @@ msgstr "" "\"\" 不合法,您可以使用 \"pixiv_downloader config fix\" 来移除所有不合" "法的值。" -#: settings.rs:386 +#: settings.rs:394 msgid "Can not convert settings to JSON object." msgstr "无法将设置转换为 JSON 对象。" -#: settings.rs:414 +#: settings.rs:422 msgid "Failed to flush file:" msgstr "无法刷新文件缓冲区:" -#: settings_list.rs:7 +#: settings_list.rs:9 msgid "Pixiv's refresh tokens. Used to login." msgstr "Pixiv 的 refresh tokens。用于登录。" @@ -307,7 +331,7 @@ msgstr "Pixiv 的 refresh tokens。用于登录。" msgid "Do you want to delete file \"\"?" msgstr "你想要删除文件 吗?" -#: utils.rs:51 webclient.rs:146 +#: utils.rs:51 webclient.rs:152 msgid "Can not parse URL:" msgstr "无法解析 URL:" @@ -315,46 +339,50 @@ msgstr "无法解析 URL:" msgid "Failed to get file name from path:" msgstr "无法从路径获取文件名:" -#: webclient.rs:106 +#: webclient.rs:112 msgid "Failed to parse Set-Cookie header." msgstr "无法解析 Set-Cookie 头部。" -#: webclient.rs:111 +#: webclient.rs:117 msgid "Failed to convert to string:" msgstr "无法转换为字符串:" -#: webclient.rs:153 +#: webclient.rs:159 msgid "Parameters should be object or array:" msgstr "参数应该是对象或数组:" -#: webclient.rs:173 +#: webclient.rs:179 msgid "Parameters should be array:" msgstr "参数应该是数组:" -#: webclient.rs:177 +#: webclient.rs:183 msgid "Parameters need at least a value:" msgstr "参数需要至少含有一个值:" -#: webclient.rs:213 +#: webclient.rs:222 +msgid "Retry after seconds." +msgstr " 秒后重试。" + +#: webclient.rs:226 msgid "Retry times now." msgstr "正在进行第 次重试。" -#: webclient.rs:227 +#: webclient.rs:240 msgid "Error when request:" msgstr "请求时发生错误:" -#: webclient.rs:288 +#: webclient.rs:301 msgid "Error when downloading file:" msgstr "下载文件时发生错误:" -#: main.rs:65 +#: main.rs:68 msgid "Failed to save config file:" msgstr "无法保存设置文件:" -#: main.rs:76 +#: main.rs:79 msgid "All available settings:" msgstr "所有可用的设置:" -#: main.rs:108 +#: main.rs:111 msgid "Can not read config file:" msgstr "无法读取设置文件:" diff --git a/src/data/data.rs b/src/data/data.rs index 382bbaf..ce56747 100644 --- a/src/data/data.rs +++ b/src/data/data.rs @@ -31,7 +31,7 @@ impl PixivData { } /// Read data from JSON object. - /// The object is from https://www.pixiv.net/artworks/ + /// The object is from `https://www.pixiv.net/artworks/` /// * `value` - The JSON object /// * `allow_overwrite` - Allow overwrite the data existing. pub fn from_web_page_data(&mut self, value: &JsonValue, allow_overwrite: bool) { diff --git a/src/data/json.rs b/src/data/json.rs index 9e80563..094ba8f 100644 --- a/src/data/json.rs +++ b/src/data/json.rs @@ -135,3 +135,15 @@ impl ToJson for String { Some(JsonValue::String(self.to_string())) } } + +impl ToJson for JsonValue { + fn to_json(&self) -> Option { + Some(self.clone()) + } +} + +impl ToJson for &JsonValue { + fn to_json(&self) -> Option { + Some((*self).clone()) + } +} diff --git a/src/dur.rs b/src/dur.rs new file mode 100644 index 0000000..d29c357 --- /dev/null +++ b/src/dur.rs @@ -0,0 +1,95 @@ +use crate::gettext; +use std::cmp::PartialEq; +use std::str::FromStr; +use std::time::Duration; + +#[allow(dead_code)] +pub enum DurType { + Second, + MilliSecond, + NanoSecond, +} + +#[derive(Clone, PartialEq)] +pub struct Dur { + num: Option, + f: Option, +} + +impl Dur { + pub fn from_num(num: u64) -> Self { + Self { + num: Some(num), + f: None, + } + } + + pub fn from_f(f: f64) -> Self { + Self { + num: None, + f: Some(f), + } + } + + pub fn to_duration(&self, t: DurType) -> Duration { + match t { + DurType::Second => { + if self.num.is_some() { + Duration::from_secs(self.num.unwrap()) + } else { + Duration::from_secs_f64(self.f.unwrap()) + } + } + DurType::MilliSecond => { + if self.num.is_some() { + Duration::from_millis(self.num.unwrap()) + } else { + Duration::from_secs_f64(self.f.unwrap() / 1000f64) + } + } + DurType::NanoSecond => { + if self.num.is_some() { + Duration::from_nanos(self.num.unwrap()) + } else { + Duration::from_secs_f64(self.f.unwrap() / 1_000_000_000f64) + } + } + } + } +} + +impl FromStr for Dur { + type Err = &'static str; + fn from_str(s: &str) -> Result { + let s = s.trim(); + let num = s.parse::(); + if num.is_ok() { + return Ok(Self::from_num(num.unwrap())); + } + let f = s.parse::(); + if num.is_ok() { + return Ok(Self::from_f(f.unwrap())); + } + Err(gettext("Failed to parse duration from string.")) + } +} + +impl PartialEq for Dur { + fn eq(&self, other: &u64) -> bool { + if self.num.is_some() { + self.num.as_ref().unwrap() == other + } else { + false + } + } +} + +impl PartialEq for Dur { + fn eq(&self, other: &f64) -> bool { + if self.f.is_some() { + self.f.as_ref().unwrap() == other + } else { + false + } + } +} diff --git a/src/list.rs b/src/list.rs new file mode 100644 index 0000000..ad19b30 --- /dev/null +++ b/src/list.rs @@ -0,0 +1,172 @@ +use std::clone::Clone; +use std::cmp::PartialEq; +use std::convert::AsMut; +use std::convert::AsRef; +use std::convert::From; +use std::convert::Into; +use std::default::Default; +use std::fmt::Debug; +use std::ops::AddAssign; +use std::ops::Index; +use std::ops::IndexMut; +use std::str::FromStr; + +/// A list which if index greater than count, which will return last one of the element. +pub struct NonTailList { + /// The list + data: Vec, +} + +impl AddAssign for NonTailList { + fn add_assign(&mut self, rhs: T) { + self.data.push(rhs) + } +} + +impl AddAssign<&T> for NonTailList { + fn add_assign(&mut self, rhs: &T) { + self.data.push(rhs.clone()) + } +} + +impl AsMut> for NonTailList { + fn as_mut(&mut self) -> &mut NonTailList { + self + } +} + +impl AsRef> for NonTailList { + fn as_ref(&self) -> &NonTailList { + self + } +} + +impl Clone for NonTailList { + fn clone(&self) -> Self { + Self { + data: self.data.clone() + } + } +} + +impl Debug for NonTailList where Vec: Debug { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Vec::::fmt(&self.data, f) + } +} + +impl Default for NonTailList { + fn default() -> Self { + Self { + data: Vec::new(), + } + } +} + +impl From<&[T]> for NonTailList { + fn from(list: &[T]) -> Self { + Self { + data: Vec::from(list), + } + } +} + +impl From> for NonTailList { + fn from(list: Vec) -> Self { + Self { + data: list, + } + } +} + +impl From<&Vec> for NonTailList { + fn from(list: &Vec) -> Self { + Self { + data: list.clone(), + } + } +} + +impl FromStr for NonTailList { + type Err = T::Err; + /// Parse a list from string + /// * `s` - a list of items separated by `,` + /// # Examples + /// ``` + /// let l: NonTailList = NonTailList::from_str("1,2,3").unwrap(); + /// ``` + fn from_str(s: &str) -> Result{ + let mut l: Vec = Vec::new(); + let r = s.split(","); + for i in r { + let i = i.trim(); + let a = i.parse::()?; + l.push(a); + } + Ok(Self { + data: l, + }) + } +} + +impl Into> for NonTailList { + fn into(self) -> Vec { + self.data + } +} + +impl Into> for &NonTailList { + fn into(self) -> Vec { + self.data.clone() + } +} + +impl Index for NonTailList { + type Output = T; + fn index(&self, index: usize) -> &Self::Output { + let count = self.data.len(); + if index < count { + &self.data[index] + } else { + &self.data[count - 1] + } + } +} + +impl IndexMut for NonTailList { + fn index_mut(&mut self, index: usize) -> &mut Self::Output { + let count = self.data.len(); + if index < count { + self.data.index_mut(index) + } else { + self.data.index_mut(count - 1) + } + } +} + +impl PartialEq> for NonTailList where T: PartialEq { + fn eq(&self, other: &Vec) -> bool { + &self.data == other + } +} + +#[test] +fn test_non_tail_list() { + let mut l = NonTailList::from(vec![1, 2, 3]); + assert_eq!(1, l[0]); + assert_eq!(2, l[1]); + assert_eq!(3, l[2]); + assert_eq!(3, l[3]); + l[0] = 0; + assert_eq!(0, l[0]); + let l2: Vec = l.as_ref().into(); + assert_eq!(l2, vec![0, 2, 3]); + assert_eq!(l[4], 3); + l += 4; + assert_eq!(l[3], 4); + let l = NonTailList::::from_str("1, 2, 3").unwrap(); + assert!(l == vec![1, 2, 3]); + assert_eq!(l, vec![1, 2, 3]); + let l2: Vec = l.as_ref().into(); + assert_eq!(l2, vec![1, 2, 3]); +} diff --git a/src/main.rs b/src/main.rs index 615b9c1..2759880 100644 --- a/src/main.rs +++ b/src/main.rs @@ -24,14 +24,17 @@ mod _exif; mod cookies; mod data; mod download; +mod dur; #[cfg(feature = "exif")] mod exif; mod i18n; +mod list; mod opthelper; mod opts; mod parser; mod pixiv_link; mod pixiv_web; +mod retry_interval; mod settings; mod settings_list; mod utils; diff --git a/src/opthelper.rs b/src/opthelper.rs index b847054..0d3b60e 100644 --- a/src/opthelper.rs +++ b/src/opthelper.rs @@ -1,5 +1,8 @@ use crate::opts::CommandOpts; +use crate::list::NonTailList; +use crate::retry_interval::parse_retry_interval_from_json; use crate::settings::SettingStore; +use std::time::Duration; /// An sturct to access all available settings/command line switches #[derive(Clone)] @@ -8,6 +11,7 @@ pub struct OptHelper<'a> { opt: &'a CommandOpts, /// Settings settings: &'a SettingStore, + default_retry_interval: NonTailList, } impl<'a> OptHelper<'a> { @@ -34,7 +38,13 @@ impl<'a> OptHelper<'a> { } pub fn new(opt: &'a CommandOpts, settings: &'a SettingStore) -> Self { - Self { opt, settings } + let mut l = NonTailList::default(); + l += Duration::new(3, 0); + Self { + opt, + settings, + default_retry_interval: l, + } } pub fn overwrite(&self) -> Option { @@ -56,4 +66,16 @@ impl<'a> OptHelper<'a> { } None } + + /// Return retry interval + pub fn retry_interval(&self) -> NonTailList { + if self.opt.retry_interval.is_some() { + return self.opt.retry_interval.as_ref().unwrap().clone(); + } + if self.settings.have("retry-interval") { + let v = self.settings.get("retry-interval").unwrap(); + return parse_retry_interval_from_json(v).unwrap(); + } + self.default_retry_interval.clone() + } } diff --git a/src/opts.rs b/src/opts.rs index 8b347d1..df14246 100644 --- a/src/opts.rs +++ b/src/opts.rs @@ -1,11 +1,14 @@ extern crate getopts; use crate::gettext; +use crate::list::NonTailList; use crate::pixiv_link::PixivID; +use crate::retry_interval::parse_retry_interval_from_str; use crate::utils::check_file_exists; use crate::utils::get_exe_path_else_current; use getopts::Options; use std::env; +use std::time::Duration; /// Command Line command #[derive(Clone, Copy, Debug, PartialEq)] @@ -52,6 +55,8 @@ pub struct CommandOpts { pub overwrite: Option, /// Max retry count. pub retry: Option, + /// Retry interval + pub retry_interval: Option>, } impl CommandOpts { @@ -66,6 +71,7 @@ impl CommandOpts { verbose: false, overwrite: None, retry: None, + retry_interval: None, } } @@ -142,6 +148,12 @@ pub fn parse_cmd() -> Option { gettext("Max retry count if request failed."), "COUNT", ); + opts.optopt( + "", + "retry-interval", + gettext("The interval (in seconds) between two retries."), + "LIST", + ); let result = match opts.parse(&argv[1..]) { Ok(m) => m, Err(err) => { @@ -239,5 +251,14 @@ pub fn parse_cmd() -> Option { } re.as_mut().unwrap().retry = Some(c.unwrap()); } + if result.opt_present("retry-interval") { + let s = result.opt_str("retry-interval").unwrap(); + let r = parse_retry_interval_from_str(s.as_str()); + if r.is_err() { + println!("{} {}", gettext("Failed to parse retry interval:"), r.unwrap_err()); + return None; + } + re.as_mut().unwrap().retry_interval = Some(r.unwrap()); + } re } diff --git a/src/pixiv_web.rs b/src/pixiv_web.rs index 5ca02af..377a5ce 100644 --- a/src/pixiv_web.rs +++ b/src/pixiv_web.rs @@ -46,6 +46,7 @@ impl<'a> PixivWebClient<'a> { if retry.is_some() { self.client.retry = retry.unwrap(); } + self.client.retry_interval = Some(self.helper.retry_interval()); self.inited = true; true } diff --git a/src/retry_interval.rs b/src/retry_interval.rs new file mode 100644 index 0000000..6cc6e2e --- /dev/null +++ b/src/retry_interval.rs @@ -0,0 +1,84 @@ +use crate::data::json::ToJson; +use crate::dur::Dur; +use crate::dur::DurType; +use crate::gettext; +use crate::list::NonTailList; +use json::JsonValue; +use std::str::FromStr; +use std::time::Duration; + +pub fn parse_retry_interval_from_str(s: &str) -> Result, &'static str> { + let l: Vec = NonTailList::::from_str(s)?.into(); + let mut r = Vec::new(); + for i in l { + r.push(i.to_duration(DurType::Second)); + } + Ok(NonTailList::from(r)) +} + +pub fn parse_retry_interval_from_json(s: T) -> Result, &'static str> { + let obj = s.to_json(); + if obj.is_none() { + return Err(gettext("Failed to get JSON object.")); + } + let obj = obj.unwrap(); + let mut r = NonTailList::::default(); + if obj.is_number() { + match obj.as_u64() { + Some(num) => { + r += Duration::new(num, 0); + } + None => { + match obj.as_f64() { + Some(num) => { + r += Duration::from_secs_f64(num); + } + None => { + return Err(gettext("Failed to parse JSON number.")); + } + } + } + } + } else if obj.is_array() { + for v in obj.members() { + if v.is_number() { + match v.as_u64() { + Some(num) => { + r += Duration::new(num, 0); + } + None => { + match v.as_f64() { + Some(num) => { + r += Duration::from_secs_f64(num); + } + None => { + return Err(gettext("Failed to parse JSON number.")); + } + } + } + } + } else { + return Err(gettext("Unsupported JSON type.")); + } + } + } else { + return Err(gettext("Unsupported JSON type.")); + } + Ok(r) +} + +pub fn check_retry_interval(s: &JsonValue) -> bool { + parse_retry_interval_from_json(s).is_ok() +} + +#[test] +fn test_parse_retry_interval() { + let l = parse_retry_interval_from_str("2, 3, 4").unwrap(); + assert_eq!(l, vec![Duration::new(2, 0), Duration::new(3, 0), Duration::new(4, 0)]); + let l = parse_retry_interval_from_json(json::parse("123").unwrap()).unwrap(); + assert_eq!(l, vec![Duration::new(123, 0)]); + let l = parse_retry_interval_from_json(json::parse("123.7").unwrap()).unwrap(); + assert_eq!(l, vec![Duration::new(123, 700_000_000)]); + let l = parse_retry_interval_from_json(json::array![123, 123.7, 230]).unwrap(); + assert_eq!(l, vec![Duration::new(123, 0), Duration::new(123, 700_000_000), Duration::new(230, 0)]); +} diff --git a/src/settings.rs b/src/settings.rs index fcf65d8..027e723 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -255,6 +255,10 @@ impl SettingJar { None } + pub fn have(&self, key: &str) -> bool { + self.settings.contains_key(key) + } + pub fn to_json(&self) -> Option { let mut v = JsonValue::new_object(); for (_, val) in self.settings.iter() { @@ -301,6 +305,10 @@ impl SettingStore { } } + pub fn have(&self, key: &str) -> bool { + self.data.have(key) + } + pub fn have_str(&self, key: &str) -> bool { let obj = self.data.get(key); if obj.is_none() { diff --git a/src/settings_list.rs b/src/settings_list.rs index b4d1eff..66ef5c7 100644 --- a/src/settings_list.rs +++ b/src/settings_list.rs @@ -1,4 +1,5 @@ use crate::gettext; +use crate::retry_interval::check_retry_interval; use crate::settings::SettingDes; use crate::settings::JsonValueType; use json::JsonValue; @@ -9,6 +10,7 @@ pub fn get_settings_list() -> Vec { 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(), SettingDes::new("retry", gettext("Max retry count if request failed."), JsonValueType::Number, Some(check_u64)).unwrap(), + SettingDes::new("retry-interval", gettext("The interval (in seconds) between two retries."), JsonValueType::Multiple, Some(check_retry_interval)).unwrap(), ] } diff --git a/src/webclient.rs b/src/webclient.rs index d860715..9ff3b40 100644 --- a/src/webclient.rs +++ b/src/webclient.rs @@ -3,17 +3,20 @@ extern crate spin_on; use crate::cookies::Cookie; use crate::cookies::CookieJar; use crate::gettext; +use crate::list::NonTailList; use crate::utils::ask_need_overwrite; use futures_util::StreamExt; use json::JsonValue; use reqwest::{Client, IntoUrl, RequestBuilder, Response}; use spin_on::spin_on; use std::collections::HashMap; +use std::convert::TryInto; use std::ffi::OsStr; use std::fs::File; use std::fs::remove_file; use std::io::Write; use std::path::Path; +use std::time::Duration; pub trait ToHeaders { fn to_headers(&self) -> Option>; @@ -76,6 +79,8 @@ pub struct WebClient { pub verbose: bool, /// Retry times, 0 means disable pub retry: u64, + /// Retry interval + pub retry_interval: Option>, } impl WebClient { @@ -86,6 +91,7 @@ impl WebClient { cookies: CookieJar::new(), verbose: false, retry: 3, + retry_interval: None, } } @@ -210,6 +216,13 @@ impl WebClient { } count += 1; if count <= self.retry { + if self.retry_interval.is_some() { + let t = self.retry_interval.as_ref().unwrap()[(count - 1).try_into().unwrap()]; + if !t.is_zero() { + println!("{}", gettext("Retry after seconds.").replace("", format!("{}", t.as_secs_f64()).as_str()).as_str()); + spin_on(tokio::time::sleep(t)); + } + } println!("{}", gettext("Retry times now.").replace("", format!("{}", count).as_str()).as_str()); } }