UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

1,236 lines (1,084 loc) 41.7 kB
/** * @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; }