cejs
Version:
A JavaScript module framework that is simple to use.
1,236 lines (1,084 loc) • 41.7 kB
JavaScript
/**
* @name WWW work crawler sub-functions
*
* @fileoverview WWW work crawler functions: part of task / control flow
*
* @since 2019/10/13 拆分自 CeL.application.net.work_crawler
*/
'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.task',
require : 'application.net.work_crawler.'
//
+ '|data.date.|application.net.Ajax.get_URL',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
}
function module_code(library_namespace) {
// requiring
var Work_crawler = library_namespace.net.work_crawler, crawler_namespace = Work_crawler.crawler_namespace;
var get_URL = this.r('get_URL'),
//
gettext = library_namespace.locale.gettext,
/** node.js file system module */
node_fs = library_namespace.platform.nodejs && require('fs');
// --------------------------------------------------------------------------------------------
// 初始化 agent。
// create and keep a new agent. 維持一個獨立的 agent。
// 以不同 agent 應對不同 host。
function setup_agent(URL, reset_cookie) {
var agent;
if (Array.isArray(URL)) {
// [ url, post_data, options ]
URL = URL[0];
}
if (URL
// restore
|| !(agent = this.default_agent)) {
agent = library_namespace.application.net.Ajax.setup_node_net(URL
|| this.base_URL);
agent.keepAlive = true;
}
var this_agent = this.get_URL_options.agent;
// 處理 cookie。
if (this_agent && this_agent.last_cookie) {
if (reset_cookie) {
delete this_agent.last_cookie;
} else if (this_agent === agent) {
// return agent;
} else {
// assert: !!this_agent.last_cookie === true
// copy cookie @ mid.js
// {Array}.last_cookie
if (agent.last_cookie) {
library_namespace.debug([ 'setup_agent: ', {
T : '原先的 agent 已存在 .last_cookie,將覆蓋設定!請回報這個錯誤!'
} ]);
// console.log(this_agent.last_cookie);
// console.trace(agent.last_cookie);
// TODO: 合併 cookie。
}
// copy cookies
agent.last_cookie = this_agent.last_cookie;
}
}
return this.get_URL_options.agent = agent;
}
// --------------------------------
// set download directory
function set_main_directory(main_directory) {
main_directory = library_namespace
// main_directory 必須以 path separator 作結。
.append_path_separator(main_directory);
if (main_directory) {
Work_crawler.prototype.main_directory = main_directory;
}
return Work_crawler.prototype.main_directory;
}
Work_crawler.set_main_directory = set_main_directory;
// --------------------------------
// fatal error throwed
Work_crawler.THROWED = typeof Symbol === 'function' ? Symbol('THROWED') : {
throwed : true
};
Work_crawler.SKIP_THIS_CHAPTER = typeof Symbol === 'function' ? Symbol('SKIP_THIS_CHAPTER')
: {
skip_this_chapter : true
};
function onwarning(warning, work_data) {
library_namespace.error(warning);
}
// for uncaught error. work_data 可能為 undefined/image_data
function onerror(error, work_data) {
process.title = this.id + ': '
// gettext_config:{"id":"error-$1"}
+ gettext('Error: %1', String(error));
// 直接丟出異常錯誤。
throw typeof error === 'object' ? error : new Error(this.id + ': '
// 先包裝成 new Error(),就不必 console.trace() 了。
+ (new Date).format('%Y/%m/%d %H:%M:%S') + ' ' + error);
// old method
if (typeof error === 'object') {
// 直接丟出異常錯誤。
throw error;
} else {
if (false) {
// 會直接 throw new Error(),就不必 console.trace() 了。
console.trace(
// typeof error === 'object' ? JSON.stringify(error) :
error);
}
throw new Error(this.id + ': '
+ (new Date).format('%Y/%m/%d %H:%M:%S') + ' ' + error);
}
// return CeL.work_crawler.THROWED;
return Work_crawler.THROWED;
}
// --------------------------------------------------------------------------------------------
/**
* 抽取出圖片伺服器列表。
*
* @param {Array|Function}server_URL
* Server URL(s) where images are stored
* @param {Function}[callback]
* @param {String}[server_file]
* catch file
*/
function set_server_list(server_URL, callback, server_file) {
if (Array.isArray(server_URL)) {
// 直接設定。
this.server_list = server_URL;
typeof callback === 'function' && callback();
return;
}
if (typeof server_URL === 'function') {
server_URL = server_URL.call(this);
}
server_URL = this.full_URL(server_URL);
var _this = this;
// 獲取圖庫伺服器列表。
this.get_URL(server_URL, function(XMLHttp, error) {
if (error) {
_this.onerror(error);
typeof callback === 'function' && callback(error);
return Work_crawler.THROWED;
}
var html = XMLHttp.responseText;
try {
_this.server_list = _this.parse_server_list(html)
// 確保有東西。
.filter(function(server) {
return !!server;
}).unique();
} catch (e) {
_this.onerror(e);
typeof callback === 'function' && callback(e);
return Work_crawler.THROWED;
}
if (_this.server_list.length > 0) {
library_namespace.log({
// gettext_config:{"id":"get-$2-servers-from-$1-$3"}
T : [ '從[%1]取得 %2 個圖片伺服器:%3', server_URL,
_this.server_list.length,
_this.server_list.join(', ') ]
});
if (server_file) {
node_fs.writeFileSync(server_file, JSON
.stringify(_this.server_list));
}
} else {
library_namespace.error([ 'set_server_list: ', {
// No server get from [%1]!
// gettext_config:{"id":"unable-to-extract-the-image-server-list-from-$1"}
T : [ '無法從[%1]抽取出圖片伺服器列表!', server_URL ]
} ]);
}
typeof callback === 'function' && callback();
}, null, true);
}
// front end #1: start downloading operation
// callback(work_data)
function start_downloading(work_id, callback) {
if (!work_id) {
library_namespace.log({
// gettext_config:{"id":"$1-work_id-not-given"}
T : [ '%1: 沒有輸入 work_id!', this.id ]
});
return;
}
if (this.charset
&& !library_namespace.character.is_loaded(this.charset)) {
// 載入需要的字元編碼。
library_namespace.character.load(this.charset, start_downloading
.bind(this, work_id, callback));
return;
}
if (this.convert_to_language) {
// CeL.CN_to_TW('简体')
library_namespace.run('extension.zh_conversion', function() {
library_namespace.zh_conversion.CN_to_TW
//
= library_namespace.CN_to_TW = library_namespace.zh_conversion
// 小說不需要轉換資訊科技相關字詞。
.generate_converter('CN_to_TW', {
file_filter : function(file) {
return !file.includes('TWPhrasesIT');
}
}
// 2022/2/12 修改字典之後可用 TWPhrasesIT。
&& null);
});
}
library_namespace.log([ this.id, ': ',
//
(new Date).format('%Y/%m/%d %H:%M:%S'), ' ', {
// 開始下載/處理
// gettext_config:{"id":"starting-«$1»-save-to-$2"}
T : [ '開始處理《%1》,儲存至 %2', work_id, this.main_directory ]
} ]);
// prepare work directory.
library_namespace.create_directory(this.main_directory);
// check if this.main_directory exists.
// e.g., set "E:\directory\" but "E:\" do not exists.
if (!library_namespace.directory_exists(this.main_directory)) {
library_namespace.error({
T : [
// gettext_config:{"id":"cannot-create-base-directory-$1"}
'Cannot create the base directory for downloading files: %1',
this.main_directory ]
});
return;
}
this.setup_value('Referer', this.base_URL);
if (!this.server_URL) {
this.parse_work_id(work_id, callback);
return;
}
var _this = this,
// host_file
server_file = this.main_directory + 'servers.json';
if (this.use_server_cache
// host_list
&& (this.server_list = library_namespace.get_JSON(server_file))) {
// use cache of host list. 不每一次重新獲取伺服器列表。
this.parse_work_id(work_id, callback);
return;
}
this.set_server_list(this.server_URL, function(error) {
if (error)
callback();
else
_this.parse_work_id(work_id, callback);
}, server_file);
}
/**
* front end #2: start get work information operation. e.g., search only, no
* download.
*
* @param {String}work_id
* 作品標題/作品名稱
* @param {Function}callback
* callback function(work_data).
* @param {Object}[options]
* 附加參數/設定特殊功能與選項
*
* @examples <code>
var work_crawler = new CeL.work_crawler(configurations);
work_crawler.data_of(work_id, function(work_data) {
console.log(work_data);
});
* </code>
*/
function start_get_data_of(work_id, callback, options) {
function start_get_data_of_callback(work_data) {
typeof callback === 'function' && callback.call(this, work_data);
}
start_get_data_of_callback.options = Object.assign({
// get_information_only
get_data_only : true
}, options);
// TODO: full test
this.start(work_id, start_get_data_of_callback);
}
// --------------------------------------------------------------------------------------------
// this.get_URL(url, callback, post_data, get_URL_options, charset)
function this_get_URL(url, callback, post_data, get_URL_options, charset) {
if (Array.isArray(url) && !post_data && !get_URL_options) {
// this.get_URL([ url, post_data, get_URL_options ])
post_data = url[1];
get_URL_options = url[2];
url = url[0];
}
// console.trace(url);
url = this.full_URL(url);
// console.trace(url);
// console.trace(this.get_URL_options);
if (get_URL_options === true) {
// this.get_URL(url, callback, post_data, true)
get_URL_options = Object.assign({
error_retry : this.MAX_ERROR_RETRY
}, this.get_URL_options);
} else if (library_namespace.is_Object(get_URL_options)) {
// this.get_URL(url, callback, post_data, get_URL_options)
var headers = Object.assign(Object.create(null),
this.get_URL_options.headers, get_URL_options.headers);
get_URL_options = Object.assign(Object.create(null),
this.get_URL_options, get_URL_options);
get_URL_options.headers = headers;
} else {
// assert: !get_URL_options === true
get_URL_options = this.get_URL_options;
}
var fetch_type = get_URL_options.fetch_type;
if (!fetch_type && this.image_types && (new RegExp('\.(?:'
// e.g., /(?:jpg|png)(?:$|\?)/i
+ Object.keys(this.image_types).join('|') + ')(?:$|\\?)', 'i'))
//
.test(url)) {
// treat as image
get_URL_options.fetch_type = 'image';
// Will set headers @ function get_URL_node()
} else if (fetch_type && fetch_type !== 'document'
&& fetch_type !== 'image') {
library_namespace.error('this_get_URL: Invalid fetch_type: '
+ fetch_type);
}
// console.trace(get_URL_options);
// console.trace(url);
// callback(result_Object, error)
get_URL(url, callback && callback.bind(this)
|| library_namespace.null_function, charset || this.charset,
post_data, get_URL_options);
}
// --------------------------------------------------------------------------------------------
// /./ doesn't include "\r", can't preserv line separator.
var PATTERN_favorite_list_token = /(?:\r?\n|^)(\s*\/\*[\s\S]*?\*\/([^\r\n]*)|[^\r\n]*)/g;
// 解析及操作列表檔案的功能。 最愛清單 / 圖書館 / 書籤 / 書庫
function parse_favorite_list(work_list_text, options) {
if (options === true) {
options = {
rearrange_list : true
};
} else {
options = library_namespace.setup_options(options);
}
var remove_list = options.remove, rearrange_list = options.rearrange_list
|| remove_list;
if (remove_list && !Array.isArray(remove_list)) {
remove_list = [ remove_list ];
}
/** {Boolean} get parsed data */
var get_parsed = options.get_parsed || rearrange_list;
var matched, work_list = [], work_hash = Object.create(null), parsed;
work_list.blank = work_list.comments = work_list.duplicated = 0;
work_list.work_indexes = [];
if (get_parsed) {
parsed = work_list.parsed = [];
parsed.duplicated = [];
parsed.line_separator = library_namespace
.determine_line_separator(work_list_text);
parsed.toString = function() {
return this.join(this.line_separator);
};
// /(?:^|\n).../ 會無限次 match '\n...',
// 故改 /(?:\n|^)
// 但這遇到 '\n...' 會少一個 ''。
if (/^\r?\n/.test(work_list_text)) {
parsed.push('');
}
}
if (!work_list_text) {
// PATTERN_favorite_list_token 會無限次 match ''。
return work_list;
}
while (matched = PATTERN_favorite_list_token.exec(work_list_text)) {
// or work id
var work_title = matched[1], remove_it = false;
if (parsed) {
// `work_title` includes "\r"
parsed.push(work_title);
}
// .trim() 會去掉 "\r", BOM (byte order mark)
work_title = work_title.trim();
// 定義列表檔案的規範,可以統合設定檔案的規範。
if (!work_title) {
// Skip blank line
work_list.blank++;
} else if (work_title.startsWith('#')
|| work_title.startsWith('//')) {
// Skip comments
work_list.comments++;
} else if (work_title.startsWith('/*')) {
work_list.comments++;
if (matched[2] && (matched[2] = matched[2].trim())) {
// gettext_config:{"id":"$1-at-the-back-of-listed-work-with-*-will-be-ignored"}
library_namespace.warn(gettext('作品列表區塊注解 "*/" 後面的"%1"會被忽略',
matched[2]));
}
} else if ((work_title in work_hash)
|| (remove_it = remove_list
&& remove_list.includes(work_title))) {
if (!remove_it)
work_list.duplicated++;
if (parsed) {
// 改變原先的 list data。
if (!remove_it)
parsed.duplicated.push(work_title);
if (rearrange_list) {
if (typeof rearrange_list === 'function') {
rearrange_list(parsed);
} else {
// comment out this work title / work id
parsed[parsed.length - 1] = '#' + parsed.at(-1);
}
}
}
} else {
// verify work titles: .unique(), 避免同一次作業中重複下載相同的作品。
work_hash[work_title] = null;
work_list.push(work_title);
}
}
// need `delete work_list.parsed` yourself
return work_list;
}
Work_crawler.parse_favorite_list = parse_favorite_list;
// parse and rearrange favorite list file
function parse_favorite_list_file(favorite_list_file_path,
rearrange_list_file) {
var work_list = library_namespace.fs_read(favorite_list_file_path);
if (!work_list) {
// 若是檔案不存在,.fs_read() 可能會回傳 undefined。
library_namespace.warn(this.id + ': '
// gettext_config:{"id":"cannot-read-series-titles-$1"}
+ gettext('無法讀取作品清單檔案:%1', favorite_list_file_path));
return [];
}
if (rearrange_list_file === undefined) {
rearrange_list_file = this.rearrange_list_file;
}
if (rearrange_list_file) {
library_namespace.debug(this.id + ': '
// gettext_config:{"id":"rearrange-series-titles-$1"}
+ gettext('重新整理作品清單檔案:%1', favorite_list_file_path));
}
work_list = parse_favorite_list(work_list.toString(), {
rearrange_list : rearrange_list_file
});
work_list.path = favorite_list_file_path;
if (rearrange_list_file) {
if (work_list.duplicated > 0) {
// console.log(work_list.parsed);
work_list.parsed = work_list.parsed.toString();
library_namespace.info(this.id
+ ': '
+ gettext(typeof rearrange_list_file === 'function'
// rearrange_list_file 整合報告
// gettext_config:{"id":"processed-$2-series-titles-$1"}
? '重新整理作品清單檔案 [%1],處理了 %2 個作品{{PLURAL:%2|標題}}。'
// gettext_config:{"id":"commented-out-$2-series-titles-$1"}
: '重新整理列表檔案 [%1],注解/排除了%2個{{PLURAL:%2|作品}}。',
favorite_list_file_path, work_list.duplicated));
library_namespace.write_file(favorite_list_file_path,
work_list.parsed);
} else {
library_namespace.debug(this.id + ': '
// gettext_config:{"id":"no-change-to-series-titles-$1"}
+ gettext('作品清單檔案未作改變:[%1]' + favorite_list_file_path));
}
// Release memory. 釋放被占用的記憶體。
delete work_list.parsed;
}
return work_list;
}
function write_favorite_list(work_list_text, favorite_list_file_path) {
if (!favorite_list_file_path)
favorite_list_file_path = this.work_list_now.path;
if (false) {
console.log([ favorite_list_file_path,
favorite_list_file_path + '.' + this.backup_file_extension,
new_work_list.toString() ]);
console.log(new_work_list);
}
// 先創目錄。
library_namespace.create_directory(favorite_list_file_path.replace(
/[^\\\/]+$/g, ''));
// backup old favorite list file 備份最後一次修改前的書籤,預防一不小心操作錯誤時還可以補救。
if (library_namespace.storage.file_exists(favorite_list_file_path)) {
library_namespace.move_file(favorite_list_file_path,
// e.g., 'favorite.txt.bak'
favorite_list_file_path + '.' + this.backup_file_extension);
}
library_namespace.write_file(favorite_list_file_path, work_list_text
.toString());
}
// --------------------------------------------------------------------------------------------
function parse_work_id(work_id, callback) {
work_id = String(work_id);
if (this.convert_id && this.convert_id[work_id]) {
// 因為 convert_id[work_id]() 可能回傳 list,因此需要以 get_work_list() 特別處理。
this.get_work_list([ work_id ], callback);
return;
}
if (work_id
// list=filename
.startsWith('l=') || node_fs.existsSync(work_id)) {
// e.g.,
// node 各漫畫網站工具檔.js l=各漫畫網站工具檔.txt
// node 各漫畫網站工具檔.js 各漫畫網站工具檔.txt
// @see http://ac.qq.com/Rank/comicRank/type/pgv
if (work_id.startsWith('l=')) {
work_id = work_id.slice('l='.length);
}
if (/\.js$/i.test(work_id)) {
library_namespace.warn(this.id + ': '
// gettext_config:{"id":"you-might-have-mistaken-the-download-tools-as-series-titles"}
+ gettext('您可能錯把下載工具檔當作了列表檔案:%1', work_id));
[ '.lst', '.txt' ].some(function(extension) {
var work_list_file = work_id.replace(/\.js$/i, extension);
if (library_namespace.storage.file_exists(work_list_file)) {
library_namespace.info(this.id + ': '
// gettext_config:{"id":"using-series-titles-$1"}
+ gettext('改為採用作品清單檔案:%1', work_list_file));
work_id = work_list_file;
return true;
}
}, this);
}
var work_list = this.parse_favorite_list_file(work_id);
this.get_work_list(work_list, callback);
return;
}
if (work_id
// 跳過來自命令列參數的手動設定。
&& !(work_id.match(/^[^=]*/)[0] in this.import_arg_hash)) {
if (false && this.need_create_ebook) {
this.get_work_list([ work_id ], callback);
} else {
// e.g.,
// node 各漫畫網站工具檔.js 12345
// node 各漫畫網站工具檔.js ABC
this.get_work(work_id, callback);
}
return;
}
library_namespace.error([ 'parse_work_id: ', {
// Invalid work id: %1
// gettext_config:{"id":"invalid-work-id-$1"}
T : [ '作品 id 無效:%1', work_id ]
} ]);
typeof callback === 'function' && callback();
}
// --------------------------------------------------------------------------------------------
// archive old work
// @since 2019/10/19 20:33:29
function check_and_archive_old_work(work_data, work_list) {
if (false) {
console.log([ this.is_finished(work_data),
work_data.last_file_modified_date, Date.now()
//
- Date.parse(work_data.last_file_modified_date) ]);
}
/**
* Warning:
* 本封存舊作品功能僅適用於會訪問到最新的章節檔案的工具檔。若工具檔不會遍歷所有章節檔案、訪問到最新的章節檔案,則得到的是錯誤的
* `work_data.last_file_modified_date`。此時必須避免執行本函數
* check_and_archive_old_work()。
*
* e.g., work_crawler/sites/comico.js work_crawler/sites/dm5.js
*/
if (false) {
console.trace([ this.archive_old_works,
this.use_finished_date_to_archive_old_works ]);
}
if (!this.archive_old_works) {
return;
}
var interval = typeof this.archive_old_works === 'string'
&& library_namespace
.to_millisecond(typeof this.archive_old_works);
if (!(interval >= library_namespace.to_millisecond('1D'))) {
// using default interval
interval = library_namespace.to_millisecond(
// test: 作品已完結 or 逾半年未下載新章節
// 有些作品的已完結標記不確實,因此不能設太短。
this.is_finished(work_data) ? '5 month' : '.5Y');
}
var latest_update = this.use_finished_date_to_archive_old_works
// 以作品完結時間為分界來封存舊作品。預設為最後一次下載時間。
&& crawler_namespace.set_last_update_Date(work_data)
// .use_finished_date_to_archive_old_works 可以用來封存剛剛下載,但已完結許久、久未更新的作品。
|| Date.parse(work_data.last_file_modified_date);
if (false) {
console.trace([ this.use_finished_date_to_archive_old_works,
latest_update, Date.now() - latest_update > interval ]);
}
if (!(Date.now() - latest_update > interval)) {
return;
}
// --------------------------------------
// console.log(work_data);
// console.log(work_list.path);
if (this.modify_work_list_when_archive_old_works
//
&& work_list && work_list.path) {
// Also remove work title + work id from work list
library_namespace.info([ this.id + ': ', {
// gettext_config:{"id":"remove-the-archived-work-from-the-list-of-works-«$1»"}
T : [ '自作品列表中刪除將封存之作品:《%1》', work_data.title || work_data.id ]
} ]);
var work_list_text = library_namespace.read_file(work_list.path);
work_list_text = work_list_text && work_list_text.toString();
var new_work_list = parse_favorite_list(work_list_text, {
get_parsed : true
// remove : [ work_data.title, work_data.id ]
}).parsed;
var last_marked_index, archived_prefix = '# '
// gettext_config:{"id":"archived"}
+ gettext.append_message_tail_space('已封存:'),
//
prefix = '# ' + gettext(
// '封存日期:%1,作品完結時間:%2',
// gettext_config:{"id":"archived-date-$1-last-download-date-$2"}
'封存日期:%1,最後一次於 %2 下載', (new Date).toISOString(),
//
library_namespace.is_Date(work_data.last_file_modified_date)
//
? work_data.last_file_modified_date.toISOString()
//
: work_data.last_file_modified_date) + new_work_list.line_separator
+ archived_prefix;
new_work_list.forEach(function(line, index) {
var work = line.trim();
if (work === work_data.input_title || work === work_data.title
|| work == work_data.id) {
new_work_list[index]
// 接連被標示的,就不一個一個標示。
= (last_marked_index + 1 === index ? archived_prefix
: prefix)
+ line;
last_marked_index = index;
}
});
this.write_favorite_list(new_work_list, work_list.path);
}
library_namespace.info([ this.id + ': ', {
// gettext_config:{"id":"archive-the-old-work-«$1»"}
T : [ '封存舊作品:《%1》', work_data.title || work_data.id ]
} ]);
// 登記作品已被封存
crawler_namespace.set_work_status(work_data, 'archived at '
+ (new Date).format());
work_data.archived = new Date;
// backup
this.save_work_data(work_data, 'check_and_archive_old_work');
// 將舊的作品搬移到 .archive_directory_name 資料夾內
// @see function remove_old_ebooks(only_id) @ work_crawler/ebook.js
var archive_directory = this.main_directory
+ this.archive_directory_name;
if (!library_namespace.directory_exists(archive_directory)) {
library_namespace.create_directory(
// 先創建封存用目錄。
archive_directory);
}
if (false) {
console.log(work_data);
console.log([ work_data.directory,
archive_directory + work_data.directory_name ]);
}
library_namespace.move_directory(work_data.directory, archive_directory
+ work_data.directory_name);
// assert: 在這之後不可再作任何 work_data.directory 下之 FSO 操作。
return true;
}
function get_work_list(work_list, callback) {
// console.log(work_list);
// 真正處理的作品數。
var work_count = 0, all_work_status = Object.create(null),
//
start_list_serial = this.start_list_serial;
// console.log(start_list_serial);
if (start_list_serial && !(start_list_serial >= 1)) {
// start_list_serial=work_title
start_list_serial = work_list.indexOf(start_list_serial);
if (start_list_serial >= 0) {
// start_list_serial starts from 1
start_list_serial++;
}
}
if (Array.isArray(this.work_list_now)
&& this.work_list_now !== work_list) {
library_namespace.error(gettext(
// gettext_config:{"id":"warning-downloading-a-list-of-works-starting-with-$2-and-length-$1.-repeating-the-download-of-the-work-list-may-cause-an-error"}
'警告:正下載以"%2"開始、長度 %1 的作品列表中。重複下載作品列表可能造成錯誤!',
this.work_list_now.length, this.work_list_now[0]));
}
this.work_list_now = work_list;
// assert: Array.isArray(work_list)
work_list.run_serial(function for_each_title(get_next_work, work_title,
this_index) {
// 解開/插入作品。
function insert_id_list(id_list) {
if (Array.isArray(id_list) && id_list.length > 0) {
library_namespace.info('get_work_list: ' + work_title
// 插入list。
+ ' → ' + id_list.join(', '));
id_list.unshift(this_index, 0);
Array.prototype.splice.apply(work_list, id_list);
}
get_next_work();
}
// work_list.list_serial: this.work_list_now.list_serial
// this_index: convert to serial, and is next index
work_list.list_serial = ++this_index;
work_title = work_title.trim();
if (!work_title
// 指定了要開始下載的列表序號。將會跳過這個訊號之前的作品。
|| /* start_list_serial > 0 && */this_index < start_list_serial) {
// 直接進入下一個作品 work_title。
get_next_work();
return;
}
// 特別處理特定id。
var id_converter = this.convert_id && this.convert_id[work_title];
if (typeof id_converter === 'function') {
// gettext_config:{"id":"using-convert_id-$1"}
library_namespace.debug(gettext('Using convert_id[%1]',
work_title), 3, 'get_work_list');
// convert special work id:
// convert_id:{id_type:function(insert_id_list,get_label){;insert_id_list(id_list);}}
// insert_id_list: 提供異序(asynchronously,不同時)使用。
// 警告: 需要自行呼叫 insert_id_list(id_list);
id_converter.call(this, insert_id_list,
crawler_namespace.get_label);
return;
}
if (library_namespace.is_Object(id_converter) && id_converter.url
&& typeof id_converter.parser === 'function') {
library_namespace.debug(
// 從指定網址 id_converter.url 得到網頁內容後,
// 丟給解析器 id_converter.parser 解析出作品列表。
// gettext_config:{"id":"using-convert_id-$1-via-url-$2"}
gettext('Using convert_id[%1] via url: %2', work_title,
id_converter.url), 3, 'get_work_list');
// convert_id:{id_type:{url:'',parser:function(html,get_label){...}}}
this.get_URL(id_converter.url,
//
function(XMLHttp) {
var id_list = id_converter.parser.call(this,
XMLHttp.responseText, crawler_namespace.get_label);
insert_id_list(id_list);
}, null, true);
return;
}
if (id_converter) {
this.onerror('get_work_list: '
// gettext_config:{"id":"invalid-id-converter-for-$1"}
+ gettext('Invalid id converter for %1', work_title),
work_title);
typeof callback === 'function' && callback(all_work_status);
return Work_crawler.THROWED;
}
work_count++;
library_namespace.log([ this.id, ': ', {
// gettext_config:{"id":"downloading-$1-$2"}
T : [ 'Downloading %1: %2', work_count
// 下載作品列表 %1:%2。
+ (work_count === this_index ? '' : '/' + this_index)
//
+ '/' + work_list.length, work_title ],
S : {
color : 'magenta',
backgroundColor : 'cyan'
}
} ]);
this.get_work(work_title, function(work_data) {
check_and_archive_old_work.call(this, work_data, work_list);
var work_status = crawler_namespace.set_work_status(work_data);
if (work_status) {
// 把需要報告的狀態export到{Array}work_status。
// assert: {Array}work_status
if (work_data.id) {
work_status.id = work_data.id;
work_status.url = this.full_URL(this.work_URL,
work_data.id);
}
work_status.title = work_data.title || work_title;
var last_update = [];
this.last_update_status_keys.forEach(function(key) {
if (work_data[key])
last_update.push(key + ': ' + work_data[key]);
});
work_status.last_update = last_update.unique().join(', ');
// console.log(work_status);
all_work_status[work_status.title] = work_status;
}
// console.trace('' + get_next_work);
get_next_work();
});
}, function all_works_done() {
delete this.work_list_now;
library_namespace.log([ this.id + ': ', {
// gettext_config:{"id":"a-total-of-$1-works-have-been-downloaded"}
T : [ '共%1個作品下載完畢。', work_list.length ]
}, (new Date).format() ]);
var work_status_titles = Object.keys(all_work_status);
if (work_status_titles.length > 0) {
library_namespace.create_directory(
// 先創建記錄用目錄。
this.main_directory + this.log_directory_name);
try {
node_fs.writeFileSync(this.main_directory
//
+ this.log_directory_name + this.report_file_JSON,
//
JSON.stringify({
date : (new Date).toISOString(),
status : all_work_status
}));
} catch (e) {
// TODO: handle exception
}
var report_file = this.main_directory + this.log_directory_name
+ this.report_file,
// 產生網頁形式的報告檔。
reports = [ '<html>', '<head>',
// http://mdn.beonex.com/en/Web_development/Historical_artifacts_to_avoid.html
// https://developer.mozilla.org/zh-TW/docs/Web_%E9%96%8B%E7%99%BC/Historical_artifacts_to_avoid
'<meta charset="UTF-8" />', '<style>',
'table{border-collapse:collapse}',
'table,th,td{border:1px solid #55f;padding:.2em}',
'</style>', '</head>', '<body>', '<h2>',
'<a href="' + this.base_URL + '">',
this.site_name || this.id, '</a>', '</h2>', '<table>',
'<tr><th>#</th><th>id</th>',
'<th>title</th><th>status</th>',
'<th>last update</th></tr>' ];
library_namespace.info([
this.id + ': ',
{
// gettext_config:{"id":"$1-works-produced-special-conditions-recorded-in-$2"}
T : [ '共%1個作品出現特殊狀況,記錄於[%2]。',
work_status_titles.length, report_file ]
} ]);
work_status_titles.forEach(function(work_title, index) {
var work_status = all_work_status[work_title];
// assert: {Array}work_status
library_namespace.info(work_title + ': '
+ work_status.join(', '));
var work_status_report = work_status.map(function(status) {
var color;
switch (status) {
case 'not found':
color = '#f44';
break;
case 'limited':
color = '#bb0';
break;
case 'finished':
color = '#88f';
break;
default:
break;
}
if (color) {
status = '<b style="color:' + color + ';">'
//
+ gettext('work_status-' + status) + '</b>';
}
return status;
});
reports.push('<tr><td>'
+ (index + 1)
+ '</td><td>'
+ (work_status.id || '')
+ '</td><td>'
+ (work_status.url ? '<a href="' + work_status.url
+ '">' + work_status.title + '</a>'
: work_status.title) + '</td><td>'
+ work_status_report.join('<br />') + '</td><td>'
+ work_status.last_update + '</td></tr>');
});
reports.push('</table>', '</body></html>');
try {
node_fs.writeFileSync(report_file, reports
.join(library_namespace.env.line_separator));
} catch (e) {
// TODO: handle exception
}
} else {
all_work_status = undefined;
}
typeof callback === 'function' && callback(all_work_status);
}, this);
}
// ----------------------------------------------------------------------------
// const
var STOP_TASK = typeof Symbol === 'function' ? Symbol('STOP_TASK') : {
stop : true
},
// const: cancel task
QUIT_TASK = typeof Symbol === 'function' ? Symbol('QUIT_TASK') : {
quit : true
};
// this.continue_arguments =
// undefined : 沒有設定特殊控制作業,正常執行或沒有作業執行中。
// [ STOP_TASK, callback_after_stopped ] : 等待作業暫停中。
// [ QUIT_TASK, callback_after_quitted ] : 等待作業取消中。
// [ work_data, chapter_NO, callback ] : 作業已經暫停,等待重啟中。
// 🛑 Stop Sign
function is_stopping_now(quit) {
return Array.isArray(this.continue_arguments)
&& this.continue_arguments[0] === (quit ? QUIT_TASK : STOP_TASK);
}
// callback: 暫停/取消作業之後執行
function stop_task(quit, callback) {
if (!this.running) {
return;
}
if (!this.continue_arguments) {
// set flag to pause / cancel task
library_namespace.info([ this.id + ': ', {
// gettext_config:{"id":"ready-to-cancel-the-download-job.-it-will-take-effect-after-downloading-this-chapter"}
T : quit ? '準備取消下載作業中,將會在下載完本章節後生效。'
// gettext_config:{"id":"prepare-to-pause-the-download.-it-will-take-effect-after-downloading-this-chapter"}
: '準備暫停下載作業中,將會在下載完本章節後生效。'
} ]);
this.continue_arguments = [ quit ? QUIT_TASK : STOP_TASK ];
if (callback) {
this.continue_arguments.push(callback);
}
// return this.continue_arguments[0];
}
if (this.is_stopping_now() || this.is_stopping_now(true)) {
// 等待作業暫停/取消中,改變作業流程控制設定。
var _arguments = this.continue_arguments;
_arguments[0] = quit ? QUIT_TASK : STOP_TASK;
if (callback) {
_arguments.push(callback);
}
// return _arguments[0];
return;
}
// assert: 作業暫停中, is stopped now
// assert: this.continue_arguments
// === [ work_data, chapter_NO, callback ]
if (!quit) {
callback && callback();
// return STOP_TASK;
return;
}
// 作業暫停中,取消作業。必須重啟作業。
var _arguments = this.continue_arguments;
this.continue_arguments = [ QUIT_TASK, callback ];
crawler_namespace.pre_get_chapter_data.apply(this, _arguments);
// return QUIT_TASK;
}
// resume
function continue_task(callback) {
if (!this.continue_arguments) {
callback && callback(this.running);
return;
}
if (this.is_stopping_now() || this.is_stopping_now(true)) {
// 等待作業暫停/取消中,改變作業流程控制設定。改成繼續下載作業。
callback && callback(null, this.continue_arguments);
this.continue_arguments.forEach(function(callback, index) {
index > 0 && callback && callback('continue');
});
// reset flow control flag
delete this.continue_arguments;
return;
}
// assert: 作業暫停中, is stopped now
// assert: this.continue_arguments
// === [ work_data, chapter_NO, callback ]
// 作業暫停中,重啟作業。繼續下載作業。
var _arguments = this.continue_arguments, work_data = _arguments[0];
// reset flow control flag
delete this.continue_arguments;
callback && callback(work_data);
library_namespace.info([ this.id + ': ', {
// gettext_config:{"id":"continue-downloading-«$1»"}
T : [ '繼續下載《%1》。', work_data.title || work_data.id ]
} ]);
crawler_namespace.pre_get_chapter_data.apply(this, _arguments);
}
// estimated time of completion 估計時間 預計剩下時間 預估剩餘時間 預計完成時間還要多久
function estimated_message(work_data, chapter_NO) {
if (!(work_data.chapter_count > chapter_NO))
return;
// 到現在使用時間 (ms)
var time_used = Date.now() - work_data.start_downloading_time,
// chapter_NO starts from 1
chapters_to_download = work_data.chapter_count - (chapter_NO - 1),
// this.start_downloading_chapter starts from 1
chapters_downloaded = chapter_NO - work_data.start_downloading_chapter,
// 預估剩餘時間 estimated time remaining (ms)
estimated_time = chapters_to_download *
// 到現在平均每個章節使用時間。
time_used / chapters_downloaded;
// 限制顯示訊息的上下限:僅此範圍內顯示訊息。
if (!(1e3 < estimated_time && estimated_time < 1e15/* Infinity */)) {
return '';
}
// gettext_config:{"id":"estimated-$1-to-download"}
return gettext('預估還需 %1 下載完本作品。', library_namespace.age_of(0,
estimated_time, {
digits : 1
}));
}
// --------------------------------------------------------------------------------------------
// export 導出.
// @instance
Object.assign(Work_crawler.prototype, {
onwarning : onwarning,
onerror : onerror,
setup_agent : setup_agent,
data_of : start_get_data_of,
is_stopping_now : is_stopping_now,
stop_task : stop_task,
continue_task : continue_task,
estimated_message : estimated_message,
// this.get_URL(url, function callback(XMLHttp) {})
get_URL : this_get_URL,
// 需要重新讀取頁面的時候使用。
REGET_PAGE : typeof Symbol === 'function' ? Symbol('REGET_PAGE') : {
REGET_PAGE : true
},
work_URL : function work_URL(work_id) {
// default work_URL: this.base_URL + work_id + '/'
return work_id + '/';
// or you may use:
return work_id + '/' + work_id + '.html';
return work_id + '.html';
},
/**
* 對於章節列表與作品資訊分列不同頁面(URL)的情況,應該另外指定 .chapter_list_URL。 e.g., <code>
chapter_list_URL : '',
chapter_list_URL : function(work_id) { return this.work_URL(work_id) + 'chapter/'; },
chapter_list_URL : function(work_id, work_data) { return [ 'url', { post_data } ]; },
</code>
*/
// 當有設定work_data.chapter_list的時候的預設函數,由 this.get_chapter_data() 呼叫。
chapter_URL : function chapter_URL(work_data, chapter_NO) {
// chapter_NO starts from 1
var chapter_data = Array.isArray(work_data.chapter_list)
&& work_data.chapter_list[chapter_NO - 1];
// console.log(work_data.chapter_list);
// console.log(chapter_data);
// console.trace(chapter_NO + '/' + work_data.chapter_list.length);
if (chapter_data && chapter_data.skip_this_chapter)
return Work_crawler.SKIP_THIS_CHAPTER;
if (typeof chapter_data === 'string') {
// treat chapter_data as chapter url
return chapter_data;
}
// e.g., work_data.chapter_list = [ chapter_data,
// chapter_data={url:'',title:'',date:new Date}, ... ]
return chapter_data && chapter_data.url;
return this.full_URL(this.work_URL, work_data.id)
+ chapter_data.url;
},
start : start_downloading,
set_server_list : set_server_list,
parse_work_id : parse_work_id,
get_work_list : get_work_list,
// 封存舊作品。
archive_old_works : false,
// 同時自作品列表中刪除將封存之作品。
modify_work_list_when_archive_old_works : true,
parse_favorite_list_file : parse_favorite_list_file,
write_favorite_list : write_favorite_list
});
// 不設定(hook)本 module 之 namespace,僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}