UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

546 lines (470 loc) 17.8 kB
/** * @name WWW work crawler sub-functions * * @fileoverview WWW work crawler functions: part of command-line arguments * * @since 2019/10/20 拆分自 CeL.application.net.work_crawler.task */ 'use strict'; // -------------------------------------------------------------------------------------------- if (typeof CeL === 'function') { // 忽略沒有 Windows Component Object Model 的錯誤。 CeL.env.ignore_COM_error = true; CeL.run({ // module name name : 'application.net.work_crawler.arguments', require : 'application.net.work_crawler.', // 設定不匯出的子函式。 no_extend : 'this,*', // 為了方便格式化程式碼,因此將 module 函式主體另外抽出。 code : module_code }); } function module_code(library_namespace) { // requiring var Work_crawler = library_namespace.net.work_crawler; var gettext = library_namespace.locale.gettext; // -------------------------------------------------------------------------------------------- /** * 正規化定義參數的規範,例如數量包含可選範圍,可用 RegExp。如'number:0~|string:/v\\d/i', * 'number:1~400|string:item1;item2;item3'。亦可僅使用'number|string'。 * * @see CeL.data.fit_filter() * @see https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/text#pattern */ function generate_argument_condition(condition) { if (library_namespace.is_Object(condition)) return condition; var condition_data = Object.create(null), matched, PATTERN = /([a-z]+)(?::(\/(\\[\s\S]|[^\/])+\/([i]*)|[^|]+))?(?:\||$)/g; while (matched = PATTERN.exec(condition)) { var type = matched[1], _condition = undefined; if (!matched[2]) { ; } else if (matched[3]) { _condition = new RegExp(matched[3], matched[4]); } else if (type === 'number' && (_condition = matched[2].match( // @see CeL.date.parse_period.PATTERN /([+\-]?\d+(?:\.\d+)?)?\s*[–~-—─~〜﹣至]\s*([+\-]?\d+(?:\.\d+)?)?/))) { _condition = { min : _condition[1] && +_condition[1], max : _condition[2] && +_condition[2] }; } else if (type === 'number' && (matched[2] === 'natural' || matched[2] === 'ℕ')) { _condition = function is_natural(value) { return value >= 1 && value === Math.floor(value); }; } else if (type === 'number' && (matched[2] === 'natural+0' || matched[2] === 'ℕ+0')) { // Naturals with zero: non-negative integers 非負整數。 _condition = function is_non_negative(value) { return value >= 0 && value === Math.floor(value); }; } else if (type === 'number' && matched[2] === 'integer') { _condition = function is_integer(value) { return value === Math.floor(value); }; } else { _condition = matched[2].split(';'); } condition_data[type] = _condition; } return condition_data; } /** * 初始設定好命令列選項之型態資料集。 * * @param {Object}[arg_hash] * 參數型態資料集。 * @param {Boolean}[append] * 添加至當前的參數型態資料集。否則會重新設定參數型態資料集。 * * @returns {Object}命令列選項之型態資料集。 */ function setup_argument_conditions(arg_hash, append) { if (append) { arg_hash = Object.assign(Work_crawler.prototype.import_arg_hash, arg_hash); } else if (arg_hash) { // default: rest import_arg_hash Work_crawler.prototype.import_arg_hash = arg_hash; } else { arg_hash = Work_crawler.prototype.import_arg_hash; } Object.keys(arg_hash).forEach(function(key) { arg_hash[key] = generate_argument_condition(arg_hash[key]); }); // console.log(arg_hash); return arg_hash; } Work_crawler.setup_argument_conditions = setup_argument_conditions; /** * 檢核 crawler 的設定參數。 * * @param {String}key * 參數名稱 * @param value * 欲設定的值 * * @returns {Boolean} true: Error occudded * * @see CeL.data.fit_filter() */ function verify_arg(key, value) { if (!(key in this.import_arg_hash)) { return true; } var type = typeof value, arg_type_data = this.import_arg_hash[key]; // console.log(arg_type_data); if (!(type in arg_type_data)) { library_namespace.warn([ 'verify_arg: ', { // gettext_config:{"id":"the-allowed-data-type-for-$1-is-$4-but-it-was-set-to-{$2}-$3"} T : [ '"%1" 這個值所允許的數值類型為 %4,但現在被設定成 {%2} %3', // key, typeof value, value, // library_namespace.is_Object(arg_type_data) // ? Object.keys(arg_type_data).map(function(type) { return gettext(type); }).join('|') : arg_type_data ] } ]); return true; } arg_type_data = arg_type_data[type]; if (Array.isArray(arg_type_data)) { if (arg_type_data.length === 1 && typeof arg_type_data[0] === 'string') { var fso_type = arg_type_data[0] .match(/^fso_(file|files|directory|directories)$/); if (fso_type) { fso_type = fso_type[1]; if (typeof value === 'string') value = value.split('|'); // assert: Array.isArray(value) var error_fso = undefined, checker = fso_type .startsWith('file') ? library_namespace.storage.file_exists : library_namespace.storage.directory_exists; if (value.some(function(fso_path) { if (!checker(fso_path)) { error_fso = fso_path; return true; } })) { library_namespace.warn([ 'verify_arg: ', { // gettext_config:{"id":"some-$2-path(s)-specified-by-$1-do-not-exist-$3"} T : [ '至少一個由「%1」所指定的%2路徑不存在:%3', key, // gettext_config:{"id":"file","mark_type":"combination_message_id"} // gettext_config:{"id":"files","mark_type":"combination_message_id"} // gettext_config:{"id":"directory","mark_type":"combination_message_id"} // gettext_config:{"id":"directories","mark_type":"combination_message_id"} gettext(fso_type), error_fso ] } ]); return true; } return; } } // e.g., "string:value1,value2" if (arg_type_data.includes(value)) { // verified return; } } else if (arg_type_data && ('min' in arg_type_data)) { // assert: type === 'number' if ((!arg_type_data.min || arg_type_data.min <= value) && (!arg_type_data.max || value <= arg_type_data.max)) { // verified return; } } else if (typeof arg_type_data === 'function') { if (arg_type_data(value)) return; } else { if (arg_type_data !== undefined) { library_namespace.warn([ 'verify_arg: ', { // gettext_config:{"id":"unable-to-process-$1-condition-with-value-type-$2"} T : [ '無法處理 "%1" 在數值類型為 %2 時之條件!', key, arg_type_data ] } ]); } // 應該修改審查條件式,而非數值本身的問題。 return; } library_namespace.warn([ 'verify_arg: ', { // gettext_config:{"id":"$1-is-set-to-the-problematic-value-{$2}-$3"} T : [ '"%1" 被設定成了有問題的值:{%2} %3', key, typeof value, value ] } ]); return true; } /** * 設定 crawler 的參數。 normalize and setup value * * @example<code> crawler.setup_value(key, value); // 應該用: this.setup_value(key, value); // 不應用: this[key] = value; delete this[key]; </code> * * @param {any} * key * @param {any} * value * * @return {String}has error */ function setup_value(key, value) { if (!key) // gettext_config:{"id":"key-value-not-given"} return '未提供鍵值'; if (library_namespace.is_Object(key)) { // assert: value === undefined value = key; for (key in value) { this.setup_value(key, value[key]); } // TODO: return error return; } // assert: typeof key === 'string' switch (key) { case 'proxy': // 使用代理伺服器 proxy_server // TODO: check .proxy library_namespace.info({ // gettext_config:{"id":"using-proxy-server-$1"} T : [ 'Using proxy server: %1', value ] }); this.get_URL_options.proxy = this[key] = value; return; case 'cookie': // set-cookie, document.cookie if (this.get_URL_options.agent) { library_namespace.merge_cookie(this.get_URL_options.agent, value); } else if (this.get_URL_options.cookie) { if (!/;\s*$/.test(this.get_URL_options.cookie)) this.get_URL_options.cookie += ';'; this.get_URL_options.cookie += value; } else { this.get_URL_options.cookie = value; } // console.trace(this.get_URL_options); return; case 'timeout': value = library_namespace.to_millisecond(value); if (!(value >= 0)) { // gettext_config:{"id":"failed-to-parse-time"} return '無法解析的時間'; } this.get_URL_options.timeout = this[key] = value; break; // case 'agent': // @see function setup_agent(URL) case 'user_agent': if (!value) { // gettext_config:{"id":"user-agent-is-not-set"} return '未設定 User-Agent。'; } this.get_URL_options.headers['User-Agent'] = this[key] = value; break; case 'Referer': if (!value // value === '': Unset Referer && value !== '') { // gettext_config:{"id":"referer-cannot-be-undefined"} return 'Referer 不可為 undefined。'; } library_namespace.debug({ // gettext_config:{"id":"configure-referer-$1"} T : [ '設定 Referer:%1', JSON.stringify(value) ] }, 2); this.get_URL_options.headers.Referer = value; // console.log(this.get_URL_options); return; case 'allow_EOI_error': if (this.using_default_MIN_LENGTH) { this[key] = value; // 因為 .allow_EOI_error 會影響到 .MIN_LENGTH this.setup_value('MIN_LENGTH', 'default'); return; } break; case 'MIN_LENGTH': // 設定預設可容許的最小圖像大小。 if (!(value >= 0)) { if (value === 'default') { this.using_default_MIN_LENGTH = true; value = this.allow_EOI_error ? 4e3 : 1e3; } else // gettext_config:{"id":"min-image-size-should-be-greater-than-0"} return '最小圖片大小應大於等於零'; } else { delete this.using_default_MIN_LENGTH; } break; case 'main_directory': if (!value || typeof value !== 'string') return; value = value.replace(/[\\\/]/g, // 正規化成當前作業系統使用的目錄分隔符號。 library_namespace.env.path_separator); // main_directory 必須以 path separator 作結。 value = library_namespace.append_path_separator(value); break; } if (key in this.import_arg_hash) { this.verify_arg(key, value); } if (value === undefined) { // delete this[key]; } this[key] = value; } // import command line arguments 以命令行參數為準 // 從命令列引數來的設定,優先等級比起作品預設設定更高。 function import_args() { // console.log(library_namespace.env.arg_hash); if (!library_namespace.env.arg_hash) { return; } for ( var key in library_namespace.env.arg_hash) { if (!(key in this.import_arg_hash) && !(key in this)) { continue; } var value = library_namespace.env.arg_hash[key]; if (this.import_arg_hash[key] === 'number') { try { // value = +value; // 這樣可以處理如"1e3" value = JSON.parse(value); } catch (e) { library_namespace.error('import_args: ' // gettext_config:{"id":"cannot-parse-$1"} + gettext('無法解析 %1', key + '=' + value)); continue; } } var old_value = this[key], error = this.setup_value(key, value); if (error) { library_namespace.error('import_args: ' // gettext_config:{"id":"unable-to-set-$1-$2"} + gettext('無法設定 %1:%2', key + '=' + old_value, error)); } else { library_namespace.log(library_namespace.display_align([ [ key + ': ', old_value ], // + ' ': 增加間隙。 // gettext_config:{"id":"from-command-line"} [ gettext('由命令列') + ' → ', value ] ])); } } } // -------------------------------------------------------------------------------------------- // export 導出. // @instance Object.assign(Work_crawler.prototype, { verify_arg : verify_arg, setup_value : setup_value, import_args : import_args, // 數值規範。命令列可以設定的選項之型態資料集。通常僅做測試微調用。 // GUI 選項於 work_crawler/gui_electron/gui_electron_functions.js 設定。 // 以純量為主,例如邏輯真假、數字、字串。無法處理函數! // 現在 import_arg_hash 之說明已與 I18n 統合在一起。 // work_crawler/work_crawler_loader.js與gui_electron_functions.js各參考了import_arg_hash的可選參數。 // @see work_crawler/gui_electron/gui_electron_functions.js // @see work_crawler/resource/locale of work_crawler - locale.csv // gettext_config:{"id":"number","mark_type":"combination_message_id"} // gettext_config:{"id":"function","mark_type":"combination_message_id"} // gettext_config:{"id":"boolean","mark_type":"combination_message_id"} // gettext_config:{"id":"string","mark_type":"combination_message_id"} // gettext_config:{"id":"fso_file","mark_type":"combination_message_id"} // gettext_config:{"id":"fso_files","mark_type":"combination_message_id"} // gettext_config:{"id":"fso_directory","mark_type":"combination_message_id"} // gettext_config:{"id":"fso_directories","mark_type":"combination_message_id"} import_arg_hash : { // 預設值設定於 Work_crawler_prototype @ CeL.application.net.work_crawler // set download directory, fso:directory main_directory : 'string:fso_directory', // crawler.show_work_data(work_data); show_information_only : 'boolean', // 一張張下載圖片。 // string: image_time_interval one_by_one : 'boolean|string', // 篩選想要下載的章節標題關鍵字。例如"單行本"。 chapter_filter : 'string', // 開始/接續下載的章節。將依類型轉成 .start_chapter_title 或 // .start_chapter_NO。對已下載過的章節,必須配合 .recheck。 start_chapter : 'number:natural|string', // 開始/接續下載的章節編號。 start_chapter_NO : 'number:natural', // 下載此章節編號範圍。例如 "20-30,50-60"。 chapter_NO_range : 'string', // 開始/接續下載的章節標題。 start_chapter_title : 'string', // 指定了要開始下載的列表序號。將會跳過這個訊號之前的作品。 // 一般僅使用於命令列設定。default:1 start_list_serial : 'number:natural|string', // 重新整理列表檔案 rearrange list file rearrange_list_file : 'boolean', // string: 如 "3s" chapter_time_interval : 'number:natural+0|string|function', MIN_LENGTH : 'number:natural+0', timeout : 'number:natural+0|string', // 容許錯誤用的相關操作設定。 MAX_ERROR_RETRY : 'number:natural+0', allow_EOI_error : 'boolean', skip_error : 'boolean', skip_chapter_data_error : 'boolean', directory_name_pattern : 'string', preserve_work_page : 'boolean', preserve_chapter_page : 'boolean', remove_ebook_directory : 'boolean', // 當新獲取的檔案比較大時,覆寫舊的檔案。 overwrite_old_file : 'boolean', // 隱藏電子書的章節資訊欄位。 hide_chapter_information : 'boolean', vertical_writing : 'boolean|string', // RTL_writing : 'boolean', convert_to_language : 'string:TW;CN', // 不解開原電子書的選項: 就算存在舊電子書檔案,也不解壓縮、利用舊資料。 discard_old_ebook_file : 'boolean', user_agent : 'string', // 代理伺服器 proxy_server: "username:password@hostname:port" proxy : 'string', // 設定下載時要添加的 cookie。 document.cookie: "key=value" cookie : 'string', // 可接受的圖片類別(延伸檔名)。以 "|" 字元作分隔,如 "webp|jpg|png"。未設定將不作檢查。 // 輸入 "images" 表示接受所有圖片。 acceptable_types : 'string', // 漫畫下載完畢後壓縮圖片檔案。 archive_images : 'boolean', // 完全沒有出現錯誤才壓縮圖片檔案。 archive_all_good_images_only : 'boolean', // 壓縮圖片檔案之後,刪掉原先的圖片檔案。 remove_images_after_archive : 'boolean', images_archive_extension : 'string', // 重新擷取用的相關操作設定。 regenerate : 'boolean', reget_chapter : 'boolean', // force: see "檢查是否久未更新。" recheck : 'boolean|string:changed;multi_parts_changed;force', search_again : 'boolean', cache_title_to_id : 'boolean', write_chapter_metadata : 'boolean', write_image_metadata : 'boolean', // 封存舊作品。 archive_old_works : 'boolean|string', // 以作品完結時間為分界來封存舊作品。預設為最後一次下載時間。 use_finished_date_to_archive_old_works : 'boolean', // 同時自作品列表中刪除將封存之作品。 modify_work_list_when_archive_old_works : 'boolean', // 儲存偏好選項 save_options。 save_preference : 'boolean' } }); setup_argument_conditions(); // 不設定(hook)本 module 之 namespace,僅執行 module code。 return library_namespace.env.not_to_extend_keyword; }