UNPKG

cejs

Version:

A JavaScript module framework that is simple to use.

850 lines (738 loc) 31.1 kB
/** * @name CeL function for downloading online works (novels, comics). * * @fileoverview 本檔案包含了批量下載網路作品(小說、漫畫)的函式庫。 WWW work crawler library. * * <code> TODO: 將設定儲存在系統預設的設定目錄 Windows: %APPDATA%\work_crawler\ UNIX: $HOME/.work_crawler/ 搜尋已下載作品 save cookie @ CLI 建造可以自動生成index/說明的工具。 自動判別網址所需要使用的下載工具,輸入網址自動揀選所需的工具檔案。 從其他的資料來源網站尋找,以獲取作品以及章節的資訊。 自動記得某個作品要從哪些網站下載。 GUI開啟錯誤紀錄 增加版本上報 漫畫下載流程教學 CLI progress bar 下載完畢後作繁簡轉換。 在單一/全部任務完成後執行的外部檔+等待單一任務腳本執行的時間(秒數) 用安全一點的 eval() Runs untrusted code securely https://github.com/patriksimek/vm2 parse 圖像。 拼接長圖之後重新分割:以整個橫切全部都是同一顏色白色為界,並且可以省略掉相同顏色的區塊。 using .epub 處理每張圖片被分割成多個小圖的情況 add .image_indexes[] ? 檢核章節內容。 考慮 search_URL 搜尋的頁數,當搜索獲得太多結果時也要包含所有結果 detect encoded data: https://gchq.github.io/CyberChef/ </code> */ // More examples: See 各網站工具檔.js: https://github.com/kanasimi/work_crawler 'use strict'; // 'use asm'; // -------------------------------------------------------------------------------------------- if (typeof CeL === 'function') { // 忽略沒有 Windows Component Object Model 的錯誤。 CeL.env.ignore_COM_error = true; CeL.run({ // module name name : 'application.net.work_crawler', // .includes() @ CeL.data.code.compatibility require : 'data.code.compatibility.' // .between() @ CeL.data.native // .append() @ CeL.data.native // .pad() @ CeL.data.native // display_align() @ CeL.data.native + '|data.native.' // for CeL.to_file_name() + '|application.net.' // for CeL.env.arg_hash, CeL.fs_read() + '|application.platform.nodejs.|application.storage.' // for CeL.storage.file.file_type() + '|application.storage.file.' // for HTML_to_Unicode() + '|interact.DOM.' // for Date.prototype.format(), String.prototype.to_Date(), // .to_millisecond() + '|data.date.' // CeL.character.load(), 僅在要設定 this.charset 時才需要載入。 + '|data.character.' // gettext(), and for .detect_HTML_language(), .time_zone_of_language() + '|application.locale.gettext' // guess_text_language() + '|application.locale.encoding.' // storage.archive() + '|application.storage.archive.', // 設定不匯出的子函式。 no_extend : '*', // 為了方便格式化程式碼,因此將 module 函式主體另外抽出。 code : module_code }); } function module_code(library_namespace) { // requiring var // library_namespace.locale.gettext gettext = this.r('gettext'), /** node.js file system module */ node_fs = library_namespace.platform.nodejs && require('fs'); // -------------------------------------------------------------------------------------------- function Work_crawler(configurations) { Object.assign(this, configurations); // 預設自動匯入 .env.arg_hash if (this.auto_import_args) this.import_args(); // 在crawler=new CeL.work_crawler({})的情況下可能沒辦法得到準確的檔案路徑,因此這個路徑僅供參考。 var main_script_path = library_namespace.get_script_base_path(/\.js/i, module); if (main_script_path) this.main_script = main_script_path; // this.id 之後將提供給 this.site_id 使用。 // 在使用gui_electron含入檔案的情況下,this.id應該稍後在設定。 if (!this.id) { this.id = this.main_script // **1** require.main.filename: 如 require('./site_id.js') // **2** 如 node site_id.js work_id && this.main_script // 去掉 path .replace(/^[\s\S]*[\\\/]([^\\\/]+)$/, '$1') // 去掉 file extension .replace(/\.*[^.]+$/, '') // NOT require('./site_id.js'). 如 node site_id.js work_id || this.main_directory.replace(/\.*[\\\/]+$/, '') // **3** others: unnormal || this.base_URL.match(/\/\/([^\/]+)/)[1].toLowerCase().split('.') // .reverse().some(function(token, index) { if (index === 0) { // 頂級域名 return false; } if (token !== 'www') { this.id = token; } if (token.length > 3 || index > 1) { // e.g., www.[id].co.jp return true; } }, this); if (!this.id && !(this.id = this.id.match(/[^\\\/]*$/)[0])) { library_namespace.error({ // gettext_config:{"id":"cannot-detect-work-id-from-url-$1"} T : [ '無法從網址擷取作品 id:%1', this.base_URL ] }); } } // gettext_config:{"id":"starting-$1"} process.title = gettext('Starting %1', this.id); if (library_namespace.is_digits(this.baidu_cse)) { if (!this.parse_search_result) { // for 百度站内搜索工具。非百度搜索系統得要自己撰寫。 this.parse_search_result = 'baidu'; } // baidu cse id 百度站内搜索工具。 if (!this.search_URL) { this.search_URL = { URL : 'http://zhannei.baidu.com/cse/search?s=' // &ie=utf-8 &isNeedCheckDomain=1&jump=1 &entry=1 + this.baidu_cse + '&q=', charset : 'UTF-8' }; } } if (typeof this.parse_search_result === 'string') { if (crawler_namespace.parse_search_result_set[this.parse_search_result]) { this.parse_search_result = crawler_namespace.parse_search_result_set[this.parse_search_result]; } else { this.onerror('Work_crawler: No this parse_search_result: ' + this.parse_search_result, work_data); return Work_crawler.THROWED; } } // 設定預設可容許的最小圖像大小。 if (!(this.MIN_LENGTH >= 0)) { // 先設定一個,預防到最後都沒有被設定到。 this.setup_value('MIN_LENGTH', 'default'); } // @see function this_get_URL() @ CeL.application.net.work_crawler.task this.get_URL_options = { // start_time : Date.now(), no_protocol_warn : true, headers : Object.assign({ // Referer will set @ start_downloading() // Referer : this.base_URL }, this.headers) }; this.setup_value('timeout', this.timeout); this.setup_value('user_agent', this.user_agent || crawler_namespace.regenerate_user_agent(this)); // console.log(this.get_URL_options); this.default_agent = this.setup_agent(); } // @inner static functions var crawler_namespace = Object.create(null); Work_crawler.crawler_namespace = crawler_namespace; // ------------------------------------------ var KEY_converted_text = 'converted_text'; // return needing to wait language converted // var promise_language = this.cache_converted_text(text_list); // if (promise_language) { return promise_language.then(); } function cache_converted_text(text_list, options) { if (!this.convert_to_language) return; var initializated = this.convert_text_language_using && this.convert_to_language_using === this.convert_to_language; if (initializated && !this.convert_text_language_using.is_asynchronous) { // 無須 cache,直接用 this.convert_text_language(text) 取得繁簡轉換過的文字即可。 return; } if (!this.converted_text_cache) { this.converted_text_cache = Object.create(null); this.converted_text_cache_persisted = Object.create(null); } if (!Array.isArray(text_list)) text_list = [ text_list ]; var _this = this, promise_list = []; text_list = text_list.filter(function(text) { // @see function convert_text_language(text, options) if (!text || !text.trim()) return false; if (text in _this.converted_text_cache) { _this.converted_text_cache[text].requiring_thread_count++; if (!_this.converted_text_cache[text][KEY_converted_text]) { promise_list.push( // _this.converted_text_cache[text].promise); } // 先前已要求過要轉換這段文字。不需要再要求一次。 return false; } else { // 正常情況: 首次要求轉換這段文字。 if (initializated) { _this.converted_text_cache[text] = { requiring_thread_count : 1 }; } // 尚未初始化的情況下,還是必須 return true 以讓 text_list.length > 0 並執行初始化。 return true; } }); // console.trace(initializated, text_list, promise_list); if (text_list.length === 0) { // !promise_list: Already cached all text needed. return promise_list.length > 0 && Promise.all(promise_list); } if (false) { console.trace(library_namespace.string_digest(text_list)); } if (initializated) { // 初始化後正常的程序。 if (false) { console.trace('Convert text:', library_namespace .string_digest(text_list)); } var promise = this.convert_text_language_using(text_list, options); // console.trace(promise); // assert: .convert_text_language_using() return thenable promise = promise // .then(function set_text_list(converted_text_list) { if (false) { console.trace('Set converted cache:', // library_namespace.string_digest(text_list), // library_namespace.string_digest(converted_text_list)); } text_list.forEach(function(text, index) { // free delete _this.converted_text_cache[text].promise; _this.converted_text_cache[text][KEY_converted_text] // assert: {Object}_this.converted_text_cache[text] // && !!converted_text_list[index] === true = converted_text_list[index]; }); // console.trace(_this.converted_text_cache); if (false) { return [ text_list, converted_text_list ]; } }); text_list.forEach(function(text) { _this.converted_text_cache[text].promise = promise; }); promise_list.push(promise); if (false) { console.trace(promise_list); } return Promise.all(promise_list); } // console.trace('cache_converted_text: 初始化 initialization'); // 僅有初始化時會執行一次。 return Promise.resolve(library_namespace.using_CeCC({ // e.g., @ function create_ebook() skip_server_test : options.skip_server_test, // 結巴中文分詞還太過粗糙,不適合依此做繁簡轉換。 try_LTP_server : true })).then(function() { _this.convert_to_language_using = _this.convert_to_language; _this.convert_text_language_using // setup this.convert_text_language_using = _this.convert_to_language === 'TW' // library_namespace.extension.zh_conversion.CN_to_TW(); ? library_namespace.CN_to_TW : library_namespace.TW_to_CN; if (false) { console.trace('cache_converted_text: 初始化完畢。'); } }).then(cache_converted_text.bind(this, text_list, options)); } // Release memory. 釋放被占用的記憶體。 function clear_converted_text_cache(options) { if (!this.convert_to_language) return; // console.trace(options); if (options === true) { options = { including_persistence : true }; } else { options = library_namespace.setup_options(options); } // ('text' in options) if (typeof options.text === 'string') { if (false) { if (!this.converted_text_cache[options.text]) { console.trace(this.converted_text_cache); console.trace(this); console.trace(options); } console.trace('Free cache of ' // + library_namespace.string_digest(options.text) // + ') requiring_thread_count=' // + (this.converted_text_cache[options.text] // && this.converted_text_cache[options.text] // .requiring_thread_count)); } // @see function cache_converted_text(text_list, options) if (options.text && options.text.trim() // && --this.converted_text_cache[options.text] // 採用 .requiring_thread_count 以避免要求轉換相同文字,後來的取用時已被刪除。 // 若相同操作會呼叫兩次 cache_converted_text(),例如初始化,則此法會出問題。 .requiring_thread_count === 0) { if (false) { console.trace('clear_converted_text_cache: Delete ' + library_namespace.string_digest(options.text)); } delete this.converted_text_cache[options.text]; } } else { // console.trace(options); if (false) { console.trace('clear_converted_text_cache: Clear all cache'); // 剩下的大多是章節名稱。 var text_list = Object.keys(this.converted_text_cache); if (text_list.length > 0) { console.trace('clear_converted_text_cache: keys lift: ' + text_list.join(', ')); } } delete this.converted_text_cache; } if (options.including_persistence) delete this.converted_text_cache_persisted; } function convert_text_language(text, options) { // @see function cache_converted_text(text_list, options) if (!text || !text.trim() || !this.convert_to_language) return text; if (!this.convert_text_language_using.is_asynchronous) return this.convert_text_language_using(text); // 當無法取得文章內容時,可能出現 this.converted_text_cache === undefined var converted_text_data = this.converted_text_cache[text]; if (converted_text_data && converted_text_data[KEY_converted_text]) { if (false && text.length !== converted_text_data[KEY_converted_text].length) { throw new Error('Different length:\n' + text + '\n' + converted_text_data[KEY_converted_text]); } if (options && options.persistence) this.converted_text_cache_persisted[text] = converted_text_data; return converted_text_data[KEY_converted_text]; } if ((text in this.converted_text_cache_persisted) && this.converted_text_cache_persisted[text][KEY_converted_text]) { return this.converted_text_cache_persisted[text][KEY_converted_text]; } if (options && options.allow_non_cache) { return text; } // console.log(this.converted_text_cache); console.trace(library_namespace.string_digest(text, 200)); console.trace(this); throw new Error( // 照理不該到這邊。 'You should run `this.cache_converted_text(text_list)` first!'); } // -------------------------------------------------------------------------------------------- // 這邊放的是一些會在 Work_crawler_prototype 中被運算到的數值。 /** {Natural}重試次數:下載失敗、出錯時重新嘗試下載的次數。同一檔案錯誤超過此數量則跳出。若值太小,在某些網站很容易出現圖片壞掉的問題。 */ Work_crawler.MAX_ERROR_RETRY = 4; Work_crawler.HTML_extension = 'htm'; // 數值規範設定於 import_arg_hash @ CeL.application.net.work_crawler.arguments var Work_crawler_prototype = { // 所有的子檔案要修訂注解說明時,應該都要順便更改在CeL.application.net.work_crawler中Work_crawler.prototype內的母comments,並以其為主體。 // 下載檔案儲存目錄路徑。 // 圖片檔與紀錄檔的下載位置。下載網路網站的作品檔案後,將儲存於此目錄下。 // 這個目錄會在 work_crawler_loader.js 裡面被 setup_crawler() 之 // global.data_directory 覆寫。 main_directory : library_namespace.storage // 決定預設的主要下載目錄 default_main_directory。 .determin_download_directory(true), // id : '', // site_id is also crower_id. // <meta name="generator" content="site_id" /> // site_id : '', // base_URL : '', // charset : 'GBK', // 預設自動匯入 .env.arg_hash auto_import_args : true, // 本工具下載時預設的使用者代理為 Chrome,所以下載的檔案格式基本上依循用 Chrome 瀏覽時的檔案格式。 // https://github.com/kanasimi/work_crawler/issues/548 // 下載每個作品更換一次 user agent。 // regenerate_user_agent : 'work', default_user_agent : 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/95.0.4638.69 Safari/537.36', // default directory_name_pattern 預設作品目錄名稱模式。 directory_name_pattern : '${id_title}${directory_name_extension}', /** * {Natural|String}timeout for get_URL() * 下載網頁或圖片的逾時等待時間。若逾時時間太小(如10秒),下載大檔案容易失敗。 * * 注意: 因為 this.get_URL_options 在 constructor 中建構完畢,因此 timeout * 會在一開始就設定。之後必須以 `this.setup_value('timeout', this.timeout);` * 設定,否則沒有效果。 */ timeout : '30s', // 本站速度頗慢,必須等待較久否則容易中斷。 // timeout : '60s', /** {Natural}重試次數:下載失敗、出錯時重新嘗試下載的次數。同一檔案錯誤超過此數量則跳出。若值太小,在某些網站很容易出現圖片壞掉的問題。 */ MAX_ERROR_RETRY : Work_crawler.MAX_ERROR_RETRY, /** {Natural}圖片下載未完全,出現 EOI (end of image) 錯誤時重新嘗試的次數。 */ MAX_EOI_ERROR : Math.min(3, Work_crawler.MAX_ERROR_RETRY), // {Natural}MIN_LENGTH:最小容許圖片檔案大小 (bytes)。 // 若值太小,傳輸到一半壞掉的圖片可能被當作正常圖片而不會出現錯誤。 // 因為當前尚未能 parse 圖像,而 jpeg 檔案可能在檔案中間出現 End Of Image mark; // 因此當圖像檔案過小,即使偵測到以 End Of Image mark 作結,依然有壞檔疑慮。 // // 對於極少出現錯誤的網站,可以設定一個比較小的數值,並且設定.allow_EOI_error=false。因為這類型的網站要不是無法獲取檔案,要不就是能夠獲取完整的檔案;要得到破損檔案,並且已通過EOI測試的機會比較少。 // MIN_LENGTH : 4e3, // 對於有些圖片只有一條細橫桿的情況。 // MIN_LENGTH : 130, // {Natural}預設所容許的章節最短內容字數。最少應該要容許一句話的長度。 MIN_CHAPTER_SIZE : 200, // {String}預設的圖片類別/圖片延伸檔名/副檔名/檔案類別/image filename extension。 default_image_extension : 'jpg', // cache directory below this.main_directory. // 必須以 path separator 作結。 cache_directory_name : library_namespace.append_path_separator('cache'), // archive directory below this.main_directory for ebook / old comics. // 封存舊電子書、舊漫畫用的目錄。 // 必須以 path separator 作結。 archive_directory_name : library_namespace .append_path_separator('archive'), // log directory below this.main_directory 必須以 path separator 作結。 log_directory_name : library_namespace.append_path_separator('log'), // 錯誤記錄檔案: 記錄無法下載的圖檔。 error_log_file : 'error_files.txt', // 當從頭開始檢查時,會重新設定錯誤記錄檔案。此時會把舊的記錄檔改名成為這個檔案。 // 移動完之後這個值會被設定為空,以防被覆寫。 error_log_file_backup : 'error_files.' + (new Date).format('%Y%2m%2dT%2H%2M%2S') + '.txt', // last updated date, latest update date. 最後更新日期時間。 // latest_chapter_url → latest_chapter_url // latest_chapter_name, last_update_chapter → latest_chapter // update_time, latest_update → last_update // 這些值會被複製到記錄報告中,並用在 show_search_result() @ gui_electron_functions.js。 last_update_status_keys : 'latest_chapter,last_update_chapter,latest_chapter,latest_chapter_name,latest_chapter_url,last_update,update_time' .split(','), // 記錄報告檔案/日誌的路徑。 report_file : 'report.' + (new Date).format('%Y%2m%2dT%2H%2M%2S') + '.' + Work_crawler.HTML_extension, report_file_JSON : 'report.json', backup_file_extension : 'bak', // default start chapter index: 1. // 將開始/接續下載的章節編號。對已下載過的章節,必須配合 .recheck。 // 若是 start_chapter 在之前下載過的最後一個章節之前的話,那麼就必須要設定 recheck 才會有效。 // 之前下載到第8章且未設定 recheck,則指定 start_chapter=9 **有**效。 // 之前下載到第8章且未設定 recheck,則指定 start_chapter=7 **無**效。必須設定 recheck。 // start_chapter : 1, start_chapter_NO : 1, // 是否重新獲取每個所檢測的章節內容 chapter_page。 // 警告: reget_chapter=false 僅適用於小說之類不獲取圖片的情形, // 因為若有圖片(parse_chapter_data()會回傳chapter_data.image_list),將把chapter_page寫入僅能從chapter_URL獲取名稱的於目錄中。 reget_chapter : true, // 是否保留 chapter page。false: 明確指定不保留,將刪除已存在的 chapter page。 // 注意: 若是沒有設定 .reget_chapter,則 preserve_chapter_page 不應發生效用。 preserve_chapter_page : false, // 是否保留作品資料 cache 於 this.cache_directory_name 下。 preserve_work_page : false, // 是否保留損壞圖檔。 preserve_bad_image : true, // 是否保留 cache // preserve_cache : true, // 當新獲取的檔案比較大時,覆寫舊的檔案。 // https://github.com/kanasimi/work_crawler/issues/242 overwrite_old_file : true, // recheck:從頭檢測所有作品之所有章節與所有圖片。不會重新擷取圖片。對漫畫應該僅在偶爾需要從頭檢查時開啟此選項。default:false // 每次預設會從上一次中斷的章節接續下載,不用特地指定 recheck。 // 有些漫畫作品分區分單行本、章節與外傳,當章節數量改變、添加新章節時就需要重新檢查/掃描。 // recheck='changed': 若是已變更,例如有新的章節,則重新下載/檢查所有章節內容。否則只會自上次下載過的章節接續下載。 // recheck='multi_parts_changed': 當有多個分部的時候才重新檢查。 // recheck : true, // recheck=false:明確指定自上次下載過的章節接續下載。 // recheck : false, // // 當無法獲取 chapter 資料時,直接嘗試下一章節。在手動+監視下 recheck 時可併用此項。 default:false // skip_chapter_data_error : true, // 重新搜尋。default:false // search_again : false, // TODO: .heif image_types : { jpg : true, jpeg : true, // 抓取到非JPG圖片 png : true, gif : true, webp : true, bmp : true }, // 漫畫下載完畢後壓縮每個章節的圖像檔案。 archive_images : true, // 完全沒有出現錯誤才壓縮圖片檔案。 // archive_all_good_images_only : true, // 壓縮圖片檔案之後,刪掉原先的圖片檔案。 請注意:必須先安裝 7-Zip **18.01 以上的版本**。 remove_images_after_archive : true, // or .cbz images_archive_extension : 'zip', // 由文章狀態/進程獲取用在作品完結的措辭。 finished_words : finished_words, is_finished : is_finished, full_URL : full_URL_of_path, convert_text_language : convert_text_language, cache_converted_text : cache_converted_text, clear_converted_text_cache : clear_converted_text_cache, // work_data properties to reset. do not inherit // 設定不繼承哪些作品資訊。 reset_work_data_properties : { limited : true, // work_data.recheck recheck : true, download_chapter_NO_list : true, // work_data.last_download last_download : true, start_chapter_NO_next_time : true, error_images : true, chapter_count : true, image_count : true } }; Object.assign(Work_crawler.prototype, Work_crawler_prototype); // Release memory. 釋放被占用的記憶體。 Work_crawler_prototype = null; // -------------------------------------------------------------------------------------------- /** * 重設瀏覽器識別 navigator.userAgent * * CeL.work_crawler.regenerate_user_agent(crawler) * * @return {String}瀏覽器識別 */ function regenerate_user_agent(crawler) { // 模擬 Chrome。 crawler.user_agent = crawler.default_user_agent // 並且每次更改不同的 user agent。 .replace(/( Chrome\/\d+\.\d+\.)(\d+)/, // function(all, main_ver, sub_ver) { return main_ver + (Math.random() * 1e4 | 0); }); return crawler.user_agent; } // -------------------------------------------------------------------------------------------- // node.innerText function get_label(html) { return html ? library_namespace.HTML_to_Unicode( html.replace(/<\!--[\s\S]*?-->/g, '').replace( /<(script|style)[^<>]*>[\s\S]*?<\/\1>/g, '').replace( /\s*<br(?:[^\w<>][^<>]*)?>[\r\n]*/ig, '\n').replace( /<\/?[a-z][^<>]*>/g, '') // incase 以"\r"為主。 e.g., 起点中文网 .replace(/\r\n?/g, '\n')).trim().replace( // \u2060: word joiner (WJ). /^\s$/.test('\uFEFF') /[\s\u200B\u200E\u200F\u2060]+$|^[\s\u200B\u200E\u200F\u2060]+/g, '') // .replace(/\s{2,}/g, ' ').replace(/\s?\n+/g, '\n') // .replace(/[\t\n]/g, ' ').replace(/ {3,}/g, ' ' + ' ') : ''; } // modify from CeL.application.net // 本函式將使用之 encodeURIComponent(),包含對 charset 之處理。 // @see function_placeholder() @ module.js crawler_namespace.encode_URI_component = function encode_URI_component( string, encoding) { if (library_namespace.character) { library_namespace.debug('採用 ' + library_namespace.Class // 有則用之。 use CeL.data.character.encode_URI_component() + '.character.encode_URI_component', 1, module_name); crawler_namespace.encode_URI_component = library_namespace.character.encode_URI_component; return crawler_namespace.encode_URI_component(string, encoding); } return encodeURIComponent(string); }; function full_URL_of_path(url, base_data, base_data_2) { if (typeof url === 'function') { url = url.call(this, base_data, base_data_2); } else if (base_data) { base_data = crawler_namespace.encode_URI_component( String(base_data), url.charset || this.charset); if (url.URL) { url.URL += base_data } else { // assert: typeof url === 'string' url += base_data; } } if (!url) { // error occurred: 未能解析出網址 return url; } // combine urls if (typeof url === 'string' && !url.includes('://')) { if (/^https?:\/\//.test(url)) { return url; } if (url.startsWith('/')) { if (url.startsWith('//')) { // 借用 base_URL 之 protocol。 return this.base_URL.match(/^(https?:)\/\//)[1] + url; } // url = url.replace(/^[\\\/]+/g, ''); // 只留存 base_URL 之網域名稱。 return this.base_URL.match(/^https?:\/\/[^\/]+/)[0] + url; } else { // 去掉開頭的 "./" url = url.replace(/^\.\//, ''); } if (url.startsWith('.')) { library_namespace.warn([ 'full_URL_of_path: ', { // gettext_config:{"id":"invalid-url-$1"} T : [ '網址無效:%1', url ] } ]); } url = this.base_URL + url; } else if (url.URL) { url.URL = this.full_URL(url.URL); } return url; } // ---------------------------------------------------------------------------- function finished_words(status) { status = String(status); // e.g., https://syosetu.org/?mode=ss_detail&nid=33378 if (/^[(\[]?(?:完[結结成]?|Completed)[)\]]?$/i.test(status)) return status; // e.g., 连载中, 連載中, 已完结, 已完成, 已完結作品, 已連載完畢, 已完/未完 // 已載完: https://www.cartoonmad.com/comic/1029.html var matched = status.match(/(?:^|已)完(?:[結结成]|$)/); if (matched) return matched[0]; // 完本: http://book.qidian.com/ if ('完結済|完本|読み切り'.split('|').some(function(word) { return status.includes(word); })) { return status; } // ck101: 全文完, 全書完 // MAGCOMI: 連載終了作品 // comico_jp: 更新終了 if (/全[文書]完|終了/.test(status)) { return status; } // 已停更 } function is_finished(work_data) { if (!work_data) return; if ('is_finished' in work_data) { return work_data.is_finished; } var status_list = library_namespace.is_Object(work_data) ? work_data.status // treat work_data as status : work_data, date; if (!status_list) { if (!this.no_checking_of_long_time_no_updated // 檢查是否久未更新。 && this.recheck && this.recheck !== 'force' && !work_data.recheck && library_namespace.is_Object(work_data) && (Date.now() // - (date = crawler_namespace.set_last_update_Date(work_data))) // 因為沒有明確記載作品是否完結,10年沒更新就不再重新下載。 / library_namespace.to_millisecond('1D') > (work_data.recheck_days || 10 * 366)) { library_namespace.info([ 'is_finished: ', { // gettext_config:{"id":"«$1»-has-not-been-updated.-$2-is-no-longer-forced-to-re-download.-it-will-only-be-re-downloaded-if-the-number-of-chapters-changes"} T : [ '《%1》已 %2 沒有更新,時間過久不再強制重新下載,僅在章節數量有變化時才重新下載。' // , work_data.title, library_namespace.age_of(date) ] } ]); work_data.recheck = 'changed'; } return status_list; } // {String|Array}status_list if (!Array.isArray(status_list)) { return this.finished_words(status_list); } var finished; if (status_list.some(function(status) { return finished = this.finished_words(status); }, this)) { return finished; } } // -------------------------------------------------------------------------------------------- // export 導出. // includes sub-modules var module_name = this.id; this.finish = function(name_space, waiting) { library_namespace.run( // @see work_crawler/*.js 'arguments,task,search,work,chapter,image,ebook'.split(',') // .map(function(name) { return module_name + '.' + name; }), waiting); return waiting; }; // @inner Object.assign(crawler_namespace, { // @see CeL.application.net.wiki PATTERN_non_CJK : /^[\u0000-\u2E7F]*$/i, get_label : get_label, regenerate_user_agent : regenerate_user_agent, // Simulates an XMLHttpRequest response. // 模擬 XMLHttpRequest response。 null_XMLHttp : { responseText : '' } }); return Work_crawler; }