permet l'ajout des frameworks et des routes

This commit is contained in:
22107988t
2023-09-25 09:41:55 +02:00
parent 0b9f7d4dfb
commit 361112699c
2787 changed files with 864804 additions and 0 deletions

3529
app/node_modules/cejs/application/net/Ajax.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

187
app/node_modules/cejs/application/net/MIME.js generated vendored Normal file
View File

@@ -0,0 +1,187 @@
/**
* @name CeL function for checking MIME type
*
* @fileoverview 本檔案包含了 checking MIME type 用的程式庫。
*
* TODO:<code>
</code>
*
* @since 2017/1/27 7:55:6
* @see https://en.wikipedia.org/wiki/Media_type
* https://www.iana.org/assignments/media-types/media-types.xhtml
* https://github.com/jshttp/mime-types
*/
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name.
name : 'application.net.MIME',
// 可以參考 CeL.application.storage.file
require : '',
// 設定不匯出的子函式。
// no_extend : '*',
// requiring
// require : '',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
/**
* null module constructor
*
* @class XML 操作相關之 function。
*/
var _// JSDT:_module_
= function() {
// null module constructor
};
/**
* for JSDT: 有 prototype 才會將之當作 Class
*/
_// JSDT:_module_
.prototype = {};
// --------------------------------------------------------------------------------------------
/**
* @see 用 CeL.application.storage.file verify_file_type() 可以驗證檔案格式。
*/
function extension_of(url) {
if (!url) {
return;
}
url = String(url);
if (url.includes('://')) {
url = url.replace(/[#?].*$/, '');
}
var matched = url.match(/\.([^.]+)$/i);
if (matched) {
if (/[a-z\d\-]+/i.test(matched[1])) {
return matched[1];
}
} else if (/^[a-z\d\-]+$/i.test(url)) {
return url;
}
}
_.file_name_extension_of = extension_of;
// 由 file name extension or url 簡易判別,可能出錯。
function MIME_type_of_extension(url, options) {
var extension = extension_of(url);
if (!extension) {
return;
}
// no .trim()
extension = extension.toLowerCase();
// common MIME types
// 常用 MIME types
switch (extension) {
// https://en.wikipedia.org/wiki/Image_file_formats
case 'jpg':
extension = 'jpeg';
case 'jpeg':
case 'png':
case 'gif':
case 'webp': // https://en.wikipedia.org/wiki/WebP
case 'bmp':
// png → image/png
return 'image/' + extension;
case 'ico':
case 'icon':
// favicon: image/vnd.microsoft.icon
return 'image/x-icon';
// ---------------------------------------------
case 'mp3':
return 'audio/mpeg';
case '3gpp':
case 'ac3':
case 'ogg':
return 'audio/' + extension;
// ---------------------------------------------
case 'avi':
case 'mp4':
case 'mpeg':
return 'video/' + extension;
// ---------------------------------------------
case 'txt':
return 'text/plain';
case 'htm':
extension = 'html';
case 'html':
//
case 'css':
case 'csv':
return 'text/' + extension;
case 'svg':
return 'image/svg+xml';
case 'xhtml':
return 'application/xhtml+xml';
case 'rtf':
case 'pdf':
case 'xml':
return 'application/' + extension;
// ---------------------------------------------
case 'otf':
case 'ttf':
case 'woff':
case 'woff2':
return 'font/' + extension;
}
}
_.MIME_of = MIME_type_of_extension;
// top-level type name
function main_MIME_type_of_extension(url) {
var type = MIME_type_of_extension(url);
if (!type) {
return;
}
var matched = type.match(/^([a-z]+)\//);
if (matched) {
return matched[1];
}
}
_.main_MIME_type_of = main_MIME_type_of_extension;
// --------------------------------------------------------------------------------------------
// export 導出.
return (_// JSDT:_module_
);
}

388
app/node_modules/cejs/application/net/archive.js generated vendored Normal file
View File

@@ -0,0 +1,388 @@
/**
* @name CeL function for checking archive sites
*
* @fileoverview 本檔案包含了 checking archive sites 用的程式庫。
*
* TODO:<code>
Memento API
https://en.wikipedia.org/wiki/Memento_Project
http://www.webcitation.org/archive
</code>
*
* @since 2016/8/6 10:4:5
* @see https://en.wikipedia.org/wiki/Archive_site
*/
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name.
name : 'application.net.archive',
// .includes() @ data.code.compatibility
// .between() @ data.native
require : 'data.code.compatibility.|data.native.'
// optional 選用:
+ '|application.net.Ajax.get_URL',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code,
// 設定不匯出的子函式。
no_extend : '*'
});
function module_code(library_namespace) {
// requiring
var get_URL = this.r('get_URL');
function archive_sites() {
}
/**
* Check if the status is OK.
*
* @param status
* status to check
*
* @returns {Boolean}the status is OK.
*/
function status_is_OK(status) {
return status >= 200 && status < 300;
}
// --------------------------------------------------------------------------------------------
// archive_org_queue = [ [ URL, {Array}callback_list ] ]
var archive_org_queue = [], archive_org_last_call,
// running now. token. 表示是否正執行中。
archive_org_running;
function archive_org_operator() {
// 已有其他 thread 執行中。
if (archive_org_running
// 已無任務。
|| archive_org_queue.length === 0) {
return;
}
// -------------------
// 確認已經等夠時間了。
var need_wait = archive_org.lag_interval
- (Date.now() - archive_org_last_call);
function to_wait() {
library_namespace.debug('Wait ' + need_wait + ' ms: [' + URL + ']',
3, 'archive_org_operator');
setTimeout(function() {
archive_org_operator();
}, need_wait);
}
if (need_wait > 0) {
to_wait();
return;
}
archive_org_last_call = Date.now();
// -------------------
archive_org_running = true;
// [ URL, {Array}callback_list ]
var checking_now = archive_org_queue.shift(),
//
URL = checking_now[0];
library_namespace.debug('Process [' + URL + '], '
+ archive_org_queue.length + ' left.', 3,
'archive_org_operator');
get_URL(archive_org.API_URL + URL, function(data, error) {
// 若正執行者,必須負責執行完註銷掉 archive_org_running。
archive_org_running = false;
if (library_namespace.is_debug(2)) {
library_namespace.debug(URL + ': '
+ (error ? 'Error: ' + error : 'OK'), 0,
'archive_org_operator');
console.log(data);
}
// 短時間內call過多次(應間隔 .5 s?)將503?
if (!error && data.status === 503) {
// rollback
archive_org_queue.unshift(checking_now);
need_wait = archive_org.lag_interval;
library_namespace.debug('Get status ' + data.status
+ '. Try again.', 3, 'archive_org_operator');
to_wait();
return;
}
function do_callback(data, error) {
library_namespace.debug(URL + ': 登記 result: ' + [ data, error ]
+ '。', 2, 'archive_org_operator');
archive_org.cached[URL] = [ data, error ];
// 執行callback
checking_now[1].forEach(function(callback) {
callback.apply(null, arguments);
});
// 執行其他剩下的。
if (archive_org_queue.length > 0) {
archive_org_operator();
}
}
if (error || !status_is_OK(data.status)) {
do_callback(undefined, error || data.status || true);
return;
}
data = JSON.parse(data.responseText);
if (!data || !(data = data.archived_snapshots.closest)
|| !data.available || !data.url) {
// 經嘗試未能取得 snapshots。
do_callback(undefined, data);
return;
}
if (!data.url.startsWith(archive_org.URL_prefix)) {
library_namespace.warn('archive_org_operator: ' + URL
+ ': archived URL does not starts with "'
+ archive_org.URL_prefix + '": ' + data.url + '.');
}
var archived_url = data.archived_url = data.url.slice(
archive_org.URL_prefix.length).between('/')
// e.g., "/index.html#", "/index.html?"
.replace(/#(.*)$/, '').replace(/\?$/, '');
if (URL !== archived_url
// 可能自動加 port。
&& URL !== (archived_url = archived_url.replace(/:\d+\//, '/'))
// 可能自動轉 https。
&& URL !== archived_url.replace('http://', 'https://')) {
library_namespace.warn('archive_org_operator: URL [' + URL
+ '] != archived [' + data.archived_url + '].');
}
do_callback(data);
}, null, null, {
// use new agent
agent : true,
no_warning : true
});
}
/**
* use Wayback Availability JSON API to check if there is any archived
* snapshot.
*
* archive.org 此 API 只能檢查是否有 snapshot不能製造 snapshot。
*
* @param {String}URL
* 欲請求之目的 URL
* @param {Function}[callback]
* 回調函數。 callback({Object|Undefined}closest_snapshot_data,
* error);
* @param {Object}[options]
* 附加參數/設定選擇性/特殊功能與選項
*
* @see https://archive.org/help/wayback_api.php
*/
function archive_org(URL, callback, options) {
var cached_data = archive_org.cached[URL];
// 看能不能直接處理掉。
if (cached_data && !cached_data.checking) {
library_namespace.debug('已登記 [' + URL + ']。直接處理掉。', 3,
'archive_org');
callback.apply(null, cached_data);
return;
}
// 登記 callback。
if (cached_data) {
cached_data[1].push(callback);
} else {
library_namespace.debug('登記 URL [' + URL + '],表示正處理中。', 3,
'archive_org');
var checking_now = [ URL, [ callback ] ];
checking_now.checking = true;
archive_org.cached[URL] = checking_now;
archive_org_queue.push(checking_now);
}
archive_org_operator();
}
/** {Natural} 延遲 time in ms。 */
archive_org.lag_interval = 500;
archive_org.API_URL = 'http://archive.org/wayback/available?url=';
/** {String}URL prefix of cached snapshot. */
archive_org.URL_prefix = 'http://web.archive.org/web/';
/** {Object} cached[URL] = [ return of archived data, error ] */
archive_org.cached = Object.create(null);
// --------------------------------------------------------------------------------------------
if (false) {
var dns = require('dns');
// 短時間內 request 過多 host names 會造成 Tool Labs 常常 DNS error
// getaddrinfo ENOTFOUND。
dns.setServers(dns.getServers().append([ '8.8.8.8', '8.8.4.4' ]));
}
/**
* 檢查 URL 之 access 狀態。若不可得,則預先測試 archive sites 是否有 cached data。
*
* @param {String}URL
* 欲請求之目的 URL
* @param {Function}[callback]
* 回調函數。 callback(link_status, cached_data);
* @param {Object}[options]
* 附加參數/設定選擇性/特殊功能與選項
*/
function check_URL(URL, callback, options) {
// normalized_URL
URL = check_URL.normalize_URL(URL);
if (!URL || !/^([a-z]+:)?\/\//i.test(URL)
|| URL.startsWith(archive_org.URL_prefix)) {
library_namespace.warn('check_URL: Cannot check [' + URL + ']');
return;
}
library_namespace.debug('check [' + URL + ']', 3, 'check_URL');
function do_callback(status, OK) {
if (!checked_URL) {
// register URL status
check_URL.link_status[URL] = status;
}
if (OK) {
callback(status);
} else {
archive_org(URL, function(closest_snapshot_data, error) {
// 會先 check archive site 再註銷此 URL
// 確保之後處理時已經有 archived data。
callback(status, closest_snapshot_data);
});
}
}
var checked_URL;
if (URL in check_URL.link_status) {
checked_URL = URL;
} else if ((checked_URL = URL.replace(':80/', '/')) in check_URL.link_status) {
// 去掉 port 80。
URL = checked_URL;
} else {
checked_URL = null;
}
if (checked_URL) {
checked_URL = check_URL.link_status[URL];
do_callback(checked_URL, status_is_OK(checked_URL));
return;
}
options = library_namespace.setup_options(options);
get_URL(URL, function(data, error) {
if (error || typeof data.responseText !== 'string') {
do_callback(error || 'check_URL: Unknown error');
return;
}
if (false && typeof options.content_processor === 'function') {
options.content_processor(
// (contains, URL, status)
data.responseText, URL, data.status);
}
if (!status_is_OK(data.status)) {
do_callback(data.status);
} else if (options.ignore_empty || data.responseText.trim()) {
do_callback(data.status, true);
} else {
do_callback('check_URL: Contents is empty');
}
}, null, null, {
content_processor : options.content_processor,
write_to_directory : options.write_to_directory,
// use new agent
agent : true,
no_warning : true,
headers : {
'User-Agent' : archive_sites.default_user_agent
}
});
}
/** {Object}check_URL.link_status[normalized_URL] = status/error */
check_URL.link_status = Object.create(null);
/**
* normalize URL to check
*
* @param {String}URL
* 欲請求之目的 URL. requested URL
*
* @returns {String}normalized_URL
*/
check_URL.normalize_URL = function(URL) {
if (!URL) {
return URL;
}
URL = String(URL);
// URL = URL.toString();
URL = URL.replace(/#.*/g, '');
try {
URL = decodeURI(URL);
} catch (e) {
}
// 去掉 default port。
URL = URL.replace(/^([^\/]*\/\/[^\/:]+):80/, '$1');
if (URL.startsWith('//')) {
// 自動加協定。
URL = 'http:' + URL;
}
return URL;
};
// --------------------------------------------------------------------------------------------
// export 導出.
Object.assign(archive_sites, {
// 為了模擬一般情況下的 access因此設定 user agent避免特殊待遇。
default_user_agent : 'Mozilla/5.0 (Windows NT 6.3)',
status_is_OK : status_is_OK,
archive_org : archive_org,
check_URL : check_URL
});
return archive_sites;
}

444
app/node_modules/cejs/application/net/wiki.js generated vendored Normal file
View File

@@ -0,0 +1,444 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科)
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用的程式庫,主要用於編寫[[維基百科:機器人]]
* ([[WP:{{{name|{{int:Group-bot}}}}}|{{{name|{{int:Group-bot}}}}}]])。
*
* TODO:<code>
wiki_API.work() 遇到 Invalid token 之類問題,中途跳出 abort 時,無法紀錄。應將紀錄顯示於 console 或 local file。
wiki_API.page() 整合各 action=query 至單一公用 function。
[[mw:Manual:Pywikibot/zh]]
[[mw:Help:OAuth]]
https://www.mediawiki.org/wiki/OAuth/Owner-only_consumers
https://meta.wikimedia.org/wiki/Steward_requests/Miscellaneous#OAuth_permissions
[[m:Special:OAuthConsumerRegistration/propose]] (using an owner-only consumers) get (consumer_key, consumer_secret, access_token, access_secret)
Wikimedia REST API
https://www.mediawiki.org/wiki/RESTBase
https://zh.wikipedia.org/w/index.php?title=title&action=history&hilight=123,456
-{zh-hans:访问;zh-hant:訪問;zh-tw:瀏覽}-量
https://wikitech.wikimedia.org/wiki/Analytics/PageviewAPI
https://en.wikipedia.org/wiki/Wikipedia:Pageview_statistics
https://dumps.wikimedia.org/other/pagecounts-raw/
https://tools.wmflabs.org/pageviews
https://wikitech.wikimedia.org/wiki/Analytics/Data/Pagecounts-raw
https://meta.wikimedia.org/wiki/Research:Page_view
WikiData Remote editor
http://tools.wmflabs.org/widar/
get user infomation:
https://www.mediawiki.org/w/api.php?action=help&modules=query%2Busers
https://zh.wikipedia.org/w/api.php?action=query&format=json&list=users&usprop=blockinfo|groups|implicitgroups|rights|editcount|registration|emailable|gender|centralids|cancreate&usattachedwiki=zhwiki&ususers=username|username
https://www.mediawiki.org/w/api.php?action=help&modules=query%2Busercontribs
https://zh.wikipedia.org/w/api.php?action=query&format=json&list=usercontribs&uclimit=1&ucdir=newer&ucprop=ids|title|timestamp|comment|parsedcomment|size|sizediff|flags|tags&ucuser=username
對Action API的更改請訂閱
https://lists.wikimedia.org/pipermail/mediawiki-api-announce/
雙重重定向/重新導向/転送
特別:二重リダイレクト
Special:DoubleRedirects
Special:BrokenRedirects
https://www.mediawiki.org/w/api.php?action=help&modules=query%2Bquerypage
[[mw:User:Duplicatebug/API Overview/action]]
https://test.wikipedia.org/w/api.php?action=query&list=querypage&qppage=DoubleRedirects&qplimit=max
gadgets 小工具 [[Wikipedia:Tools]], [[Category:Wikipedia scripts]], [[mw:ResourceLoader/Core modules]]
[[Special:MyPage/common.js]] [[使用說明:維基用戶腳本開發指南]]
// ---------------------------------------------------------
// https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.loader
mediaWiki.loader.load('https://kanasimi.github.io/CeJS/ce.js')
CeL.run('application.net.wiki');
CeL.wiki.page('Wikipedia:機器人',function(page_data){console.log(page_data);},{redirects:true,section:0})
// wikibits從2013年就棄用
// https://www.mediawiki.org/wiki/ResourceLoader/Legacy_JavaScript#wikibits.js
// NG: importScript('User:cewbot/*.js');
你可以在維基媒體的wiki網站URL最後增加?safemode=1來關閉你個人的CSS和JavaScript。範例https://zh.wikipedia.org/wiki/文學?safemode=1。上面一行意思是你可以測試是否是你的使用者腳本或套件造成問題而不必解除安裝。
</code>
*
* @see https://github.com/siddharthvp/mwn
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
// JavaScript MediaWiki API for ECMAScript 2017+ :
// https://github.com/kanasimi/wikiapi
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki',
// .includes() @ CeL.data.code.compatibility
// .between() @ CeL.data.native
// .append() @ CeL.data.native
require : 'data.code.compatibility.|data.native.'
// (new Date).format('%4Y%2m%2d'), (new Date).format() @ CeL.data.date
// optional 選用: .show_value() @ CeL.interact.DOM, CeL.application.debug
// optional 選用: CeL.wiki.cache(): CeL.application.platform.nodejs.fs_mkdir()
// optional 選用: CeL.wiki.traversal(): CeL.application.platform.nodejs
// optional 選用: wiki_API.work(): gettext():
// optional 選用: CeL.application.storage
// CeL.application.locale.gettext()
// CeL.date.String_to_Date(), Julian_day(), .to_millisecond(): CeL.data.date
+ '|data.date.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
// https://github.com/Microsoft/TypeScript/wiki/JSDoc-support-in-JavaScript
/**
* web Wikipedia / 維基百科 用的 functions。<br />
* 可執行環境: node.js, JScript。
*
* TODO: new wiki_API(API_URL || login_options);<br />
* wiki_session.login(user_name, password, API_URL);
*
* @param {String}user_name
* user name
* @param {String}password
* user password
* @param {String}[API_URL]
* language code or API Endpoint URL
*
* @returns {wiki_API} wiki site API
* @template wiki_API
*
* @constructor
*/
function wiki_API(user_name, password, API_URL) {
if (!this || this.constructor !== wiki_API) {
return wiki_API.query.apply(null, arguments);
}
// TODO: this.login(user_name, password, API_URL);
var login_options;
if (API_URL && typeof API_URL === 'object') {
// session = new wiki_API(user_name, password, login_options);
login_options = API_URL;
API_URL = null;
} else if (!API_URL && !password && user_name
&& typeof user_name === 'object') {
// session = new wiki_API(login_options);
login_options = user_name;
user_name = null;
// console.log(login_options);
} else {
login_options = Object.create(null);
}
user_name = user_name || login_options.user_name;
password = password || login_options.password;
API_URL = API_URL || login_options.API_URL/* || login_options.project */;
// console.trace([ user_name, password, API_URL ]);
library_namespace.debug('URL of service endpoint: ' + API_URL
+ ', default language: ' + wiki_API.language, 3, 'wiki_API');
// action queue 佇列。應以 append而非整個換掉的方式更改。
this.actions = [];
// @see wiki_API.prototype.next
if (login_options.is_running) {
// Is calling from wiki_API.login()
// login 前便執行其他作業,可能導致 Session=deleted。 e.g., running
// login_options.configuration_adapter() @ 20201008.fix_anchor.js
if (typeof login_options.is_running === 'string')
this.actions.unshift([ login_options.is_running ]);
// 執行權交給 wiki_API.login()。
this.running = true;
}
// 權杖
this.token = {
// lgusername
lgname : user_name,
// user_password
lgpassword : password
};
// console.trace(API_URL);
if (!API_URL && !('language' in this)
// wikidata 不設定 language。
&& !this[wiki_API.KEY_HOST_SESSION]) {
API_URL = wiki_API.language;
// 假若未設定 API_URL 或 user_name那就不初始化。等 .login 才初始化。
// 若想基本的初始化,最起碼必須設定 API_URL。
login_options.need_initialize = password && user_name;
} else if (!('need_initialize' in login_options)) {
login_options.need_initialize = true;
}
if ('use_SQL' in login_options) {
this.use_SQL = login_options.use_SQL;
} else if (API_URL
// assert: typeof API_URL === 'string'
&& API_URL.includes('://')) {
// assert: Not MediaWiki server. Is outer server.
this.use_SQL = false;
}
// console.trace(API_URL);
// setup session.
if (API_URL) {
// e.g., 'cmn'
if (API_URL in wiki_API.language_code_to_site_alias)
API_URL = wiki_API.language_code_to_site_alias[API_URL];
wiki_API.setup_API_language(this /* session */, API_URL);
wiki_API.setup_API_URL(this /* session */, API_URL);
}
[ 'site_name', 'data_API_URL', 'SPARQL_API_URL',
// Must after wiki_API.setup_API_language()!
'language' ]
//
.forEach(function(property) {
if (property in login_options)
this[property] = login_options[property];
}, this);
// console.trace(this);
this.general_parameters = Object.clone(wiki_API.general_parameters);
library_namespace.import_options(login_options,
// @see CeL.application.net.wiki.namespace
wiki_API.general_parameters_normalizer, this.general_parameters);
if (library_namespace.is_WWW(true) && window.location
// For non-authenticated requests, specify the value *. This
// will cause the Access-Control-Allow-Origin header to be set,
// but Access-Control-Allow-Credentials will be false and all
// user-specific data will be restricted.
&& this.general_parameters.origin !== '*') {
var host;
if (!window.location.host
// e.g., locale file: window.location.host===""
|| (host = new URL(this.API_URL).host)
&& host !== window.location.host
&& host !== this.general_parameters.origin) {
library_namespace.warn([ 'wiki_API: ', {
// gettext_config:{"id":"you-may-need-to-set-$1-=-$2"}
T : [ 'You may need to set %1 = %2!',
//
'.origin', JSON.stringify(host) ]
} ]);
}
}
if (login_options.localStorage_prefix_key && wiki_API.has_storage) {
// assert: typeof login_options.localStorage_prefix_key === 'string'
// ||
// typeof login_options.localStorage_prefix_key === 'number'
this.localStorage_prefix = [ library_namespace.Class,
wiki_API.site_name(this),
login_options.localStorage_prefix_key, '' ]
// '.'
.join(library_namespace.env.module_name_separator);
}
// ------------------------------------------------
// pre-loading functions
// https://stackoverflow.com/questions/39007637/javascript-set-vs-array-performance
// https://jsbench.me/3pkjlwzhbr/1
// .API_parameters[modules.path].parameter_Map = parameter Map
// @see get_API_parameters()
this.API_parameters = Object.create(null);
// wiki_session.redirects_data[redirect_from] = {String}redirect_to
// = main page title without "Template:" prefix
// @see CeL.application.net.wiki.task ,
// CeL.application.net.wiki.namespace
this.redirects_data = Object.create(null);
if (login_options.need_initialize) {
this.run_after_initializing = [];
// 注意: new wiki_API() 後之操作,應該採 wiki_session.run()
// 的方式,確保此時已經執行過 pre-loading functions @ function wiki_API():
// wiki_session.siteinfo(), wiki_session.adapt_task_configurations()
this.run(initialize_wiki_API, login_options);
} else {
// e.g.,
// wiki = new CeL.wiki; ...; wiki.login(login_options);
}
}
function initialize_wiki_API(options) {
var session = this;
// console.trace(session.actions);
// console.trace(session.running);
// if (session.API_URL)
session.siteinfo(load_template_functions);
// console.trace(session.actions);
// console.trace(session.running);
function load_template_functions() {
// console.trace(session);
// @see CeL.application.net.wiki.template_functions
if (session.load_template_functions)
session.load_template_functions(null,
//
adapt_task_configurations);
else
adapt_task_configurations();
}
function adapt_task_configurations() {
// console.trace(options);
if (options.task_configuration_page) {
session.adapt_task_configurations(
options.task_configuration_page,
function(configuration) {
// console.trace(configuration);
if (options.configuration_adapter)
options.configuration_adapter(configuration);
initialization_complete();
});
} else {
initialization_complete();
}
}
function initialization_complete() {
library_namespace.debug(wiki_API.site_name(session) + ': '
+ '初始化程序登錄完畢。' + '添加之前登錄的 ' + session.actions.length
+ ' 個程序到佇列中。', 1, 'initialization_complete');
session.actions.append(session.run_after_initializing);
delete session.run_after_initializing;
// console.trace(session.actions);
}
}
initialize_wiki_API.is_initializing_process = true;
/**
* 檢查若 value 為 session。
*
* @param value
* value to test. 要測試的值。
*
* @returns {Boolean} value 為 session。
*/
function is_wiki_API(value) {
return value
&& ((value instanceof wiki_API) || value.API_URL && value.token);
}
// ------------------------------------------------------------------------
// export 導出.
// @static
Object.assign(wiki_API, {
is_wiki_API : is_wiki_API
});
if (library_namespace.is_WWW(true) && typeof mw === 'object' && mw
&& typeof mw.config === 'object'
&& typeof mw.config.get === 'function'
&& typeof mediaWiki === "object" && mediaWiki === mw) {
wiki_API.mw_web_session = true;
}
// 等執行再包含入必須的模組。
this.finish = function(name_space, waiting, sub_modules_to_full_module_path) {
var sub_modules = [ 'namespace', 'parser', 'query', 'page',
'page.Page', 'Flow', 'list', 'edit', 'task', 'parser.wikitext',
'parser.section', 'parser.misc', 'parser.evaluate' ];
// ------------------------------------------------------------------------
// auto import SQL 相關函數 @ Toolforge。
// function setup_wmflabs()
// only for node.js.
// https://wikitech.wikimedia.org/wiki/Help:Toolforge/FAQ#How_can_I_detect_if_I.27m_running_in_Cloud_VPS.3F_And_which_project_.28tools_or_toolsbeta.29.3F
if (library_namespace.platform.nodejs) {
/** {String}Wikimedia Toolforge name. CeL.wiki.wmflabs */
wiki_API.wmflabs = require('fs').existsSync('/etc/wmflabs-project')
// e.g., 'tools-bastion-05'.
// if use `process.env.INSTANCEPROJECT`,
// you may get 'tools' or 'tools-login'.
&& (library_namespace.env.INSTANCENAME
// 以 /usr/bin/jsub 執行時可得。
// e.g., 'tools-exec-1210.eqiad.wmflabs'
|| library_namespace.env.HOSTNAME || true);
}
if (wiki_API.wmflabs) {
// import CeL.application.net.wiki.Toolforge
sub_modules.push('Toolforge');
}
// --------------------------------------------------------------------
// Essential dependency chain
library_namespace.debug({
T :
// gettext_config:{"id":"load-the-main-functions-and-necessary-dependencies-to-operate-mediawiki"}
'Load the main functions and necessary dependencies to operate MediaWiki.'
}, 1, 'wiki_API');
// library_namespace.set_debug(2);
library_namespace.run(sub_modules_to_full_module_path(sub_modules),
// The `wiki_API.mw_web_session` is a session that operates in a web
// environment. For example, the Wikipedia widget.
function() {
if (wiki_API.mw_web_session) {
wiki_API.mw_web_session = new wiki_API({
API_URL :
// mediaWiki.config.get('wgServer')
location.origin
// https://www.mediawiki.org/wiki/Manual:$wgScriptPath
+ mediaWiki.config.get('wgScriptPath')
// https://www.mediawiki.org/wiki/Manual:Api.php
+ '/api.php',
localStorage_prefix_key : 'mw_web_session'
});
// fill tokens
for ( var token_name in mediaWiki.user.tokens.values) {
wiki_API.mw_web_session.token[
// 'csrfToken' → 'csrftoken'
token_name.toLowerCase()]
//
= mediaWiki.user.tokens.values[token_name];
}
// 預設對所有網站會使用相同的 cookie
// @see
// https://doc.wikimedia.org/mediawiki-core/master/js/#!/api/mw.Api
}
library_namespace.debug({
// gettext_config:{"id":"all-wiki-submodules-are-loaded"}
T : 'All wiki submodules are loaded.'
}, 1, 'wiki_API');
}, waiting);
return waiting;
};
return wiki_API;
}

412
app/node_modules/cejs/application/net/wiki/Flow.js generated vendored Normal file
View File

@@ -0,0 +1,412 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): Flow, Structured
* Discussions
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
</code>
*
* @since 2019/10/11 拆分自 CeL.application.net.wiki
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.Flow',
require : 'data.native.' + '|application.net.wiki.'
// load MediaWiki module basic functions
+ '|application.net.wiki.namespace.'
//
+ '|application.net.wiki.query.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki, KEY_SESSION = wiki_API.KEY_SESSION;
// @inner
var is_api_and_title = wiki_API.is_api_and_title, normalize_title_parameter = wiki_API.normalize_title_parameter;
// --------------------------------------------------------------------------------------------
// Flow page support. Flow 功能支援。
// [[mediawikiwiki:Extension:Flow/API]]
// https://www.mediawiki.org/w/api.php?action=help&modules=flow
// https://zh.wikipedia.org/w/api.php?action=query&prop=flowinfo&titles=Wikipedia_talk:Flow_tests
// https://zh.wikipedia.org/w/api.php?action=query&prop=info&titles=Wikipedia_talk:Flow_tests
// https://zh.wikipedia.org/w/api.php?action=flow&submodule=view-topiclist&page=Wikipedia_talk:Flow_tests&vtlformat=wikitext&utf8=1
// .roots[0]
// https://zh.wikipedia.org/w/api.php?action=flow&submodule=view-topic&page=Topic:sqs6skdav48d3xzn&vtformat=wikitext&utf8=1
// https://www.mediawiki.org/w/api.php?action=flow&submodule=view-header&page=Talk:Sandbox&vhformat=wikitext&utf8=1
// https://www.mediawiki.org/w/api.php?action=flow&submodule=view-topiclist&utf8=1&page=Talk:Sandbox
/**
* get the infomation of Flow.
*
* @param {String|Array}title
* page title 頁面標題。可為話題id/頁面標題+話題標題。<br />
* {String}title or [ {String}API_URL, {String}title or
* {Object}page_data ]
* @param {Function}callback
* 回調函數。 callback({Object}page_data)
* @param {Object}[options]
* 附加參數/設定選擇性/特殊功能與選項
*/
function Flow_info(title, callback, options) {
var action = normalize_title_parameter(title, options);
if (!action) {
throw 'Flow_info: Invalid title: ' + wiki_API.title_link_of(title);
}
// [[mw:Extension:StructuredDiscussions/API#Detection]]
// 'prop=flowinfo' is deprecated. use 'action=query&prop=info'.
// The content model will be 'flow-board' if it's enabled.
action[1] = 'action=query&prop=info&' + action[1];
wiki_API.query(action, typeof callback === 'function'
//
&& function(data) {
if (library_namespace.is_debug(2)
// .show_value() @ interact.DOM, application.debug
&& library_namespace.show_value)
library_namespace.show_value(data, 'Flow_info: data');
var error = data && data.error;
// 檢查伺服器回應是否有錯誤資訊。
if (error) {
library_namespace.error('Flow_info: ['
//
+ error.code + '] ' + error.info);
/**
* e.g., Too many values supplied for parameter 'pageids': the
* limit is 50
*/
if (data.warnings
//
&& data.warnings.query && data.warnings.query['*'])
library_namespace.warn(data.warnings.query['*']);
callback(data, error);
return;
}
if (!data || !data.query || !data.query.pages) {
library_namespace.warn('Flow_info: Unknown response: ['
//
+ (typeof data === 'object'
//
&& typeof JSON !== 'undefined'
//
? JSON.stringify(data) : data) + ']');
if (library_namespace.is_debug()
// .show_value() @ interact.DOM, application.debug
&& library_namespace.show_value)
library_namespace.show_value(data);
callback(null, data);
return;
}
// TODO: data.query.normalized=[{from:'',to:''},...]
data = data.query.pages;
var pages = [];
for ( var pageid in data) {
var page = data[pageid];
pages.push(page);
}
// options.multi: 即使只取得單頁面,依舊回傳 Array。
if (!options || !options.multi)
if (pages.length <= 1) {
if (pages = pages[0])
pages.is_Flow = is_Flow(pages);
library_namespace.debug('只取得單頁面 [[' + pages.title
//
+ ']],將回傳此頁面資料,而非 Array。', 2, 'Flow_info');
} else {
library_namespace.debug('Get ' + pages.length
//
+ ' page(s)! The pages'
//
+ ' will all passed to callback as Array!'
//
, 2, 'Flow_info');
}
/**
* page 之 structure 將按照 wiki API 本身之 return<br />
* <code>
page_data = {ns,title,missing:'']}
page_data = {pageid,ns,title,flowinfo:{flow:[]}}
page_data = {pageid,ns,title,flowinfo:{flow:{enabled:''}}}
* </code>
*/
callback(pages);
}, null, options);
}
/**
* 檢測 page_data 是否為 Flow 討論頁面系統。
*
* other contentmodel: "MassMessageListContent"
*
* @param {Object}page_data
* page data got from wiki API.
*
* @returns {Boolean}是否為 Flow 討論頁面。
*/
function is_Flow(page_data) {
if ('contentmodel' in page_data) {
// used in prop=info
return page_data.contentmodel === 'flow-board';
}
var flowinfo = page_data &&
// wiki_API.is_page_data(page_data) &&
page_data.flowinfo;
if (flowinfo) {
// used in prop=flowinfo (deprecated)
// flowinfo:{flow:{enabled:''}}
return flowinfo.flow && ('enabled' in flowinfo.flow);
}
// e.g., 從 wiki_API.page 得到的 page_data
if (page_data = wiki_API.content_of.revision(page_data))
return (page_data.contentmodel || page_data.slots
&& page_data.slots.main
&& page_data.slots.main.contentmodel) === 'flow-board';
}
/** {Object}abbreviation 縮寫 */
var Flow_abbreviation = {
// https://www.mediawiki.org/w/api.php?action=help&modules=flow%2Bview-header
// 關於討論板的描述。使用 .revision
header : 'h',
// https://www.mediawiki.org/w/api.php?action=help&modules=flow%2Bview-topiclist
// 討論板話題列表。使用 .revisions
topiclist : 'tl'
};
/**
* get topics of the page.
*
* @param {String|Array}title
* page title 頁面標題。可為話題id/頁面標題+話題標題。 {String}title or [
* {String}API_URL, {String}title or {Object}page_data ]
* @param {Function}callback
* 回調函數。 callback({Object}topiclist)
* @param {Object}[options]
* 附加參數/設定選擇性/特殊功能與選項
*/
function Flow_page(title, callback, options) {
// 處理 [ {String}API_URL, {String}title or {Object}page_data ]
if (!is_api_and_title(title)) {
title = [ options[KEY_SESSION] && options[KEY_SESSION].API_URL,
title ];
}
var page_data;
if (wiki_API.is_page_data(title[1]))
page_data = title[1];
title[1] = 'page=' + encodeURIComponent(wiki_API.title_of(title[1]));
if (options && options.redirects) {
// 舊版毋須 '&redirects=1''&redirects' 即可。
title[1] += '&redirects=1';
}
// e.g., { flow_view : 'header' }
var view = options && options.flow_view
//
|| Flow_page.default_flow_view;
title[1] = 'action=flow&submodule=view-' + view + '&v'
+ (Flow_abbreviation[view] || view.charAt(0).toLowerCase())
+ 'format=' + (options && options.format || 'wikitext') + '&'
+ title[1];
if (!title[0])
title = title[1];
wiki_API.query(title, typeof callback === 'function'
//
&& function(data) {
if (library_namespace.is_debug(2)
// .show_value() @ interact.DOM, application.debug
&& library_namespace.show_value)
library_namespace.show_value(data, 'Flow_page: data');
var error = data && data.error;
// 檢查伺服器回應是否有錯誤資訊。
if (error) {
library_namespace.error(
//
'Flow_page: [' + error.code + '] ' + error.info);
callback(page_data);
return;
}
// data =
// { flow: { 'view-topiclist': { result: {}, status: 'ok' } } }
if (!(data = data.flow)
//
|| !(data = data['view-' + view]) || data.status !== 'ok') {
library_namespace.error(
//
'Flow_page: Error status [' + (data && data.status) + ']');
callback(page_data);
return;
}
if (page_data)
// assert: data.result = { ((view)) : {} }
Object.assign(page_data, data.result);
else
page_data = data.result[view];
callback(page_data);
}, null, options);
}
/** {String}default view to flow page */
Flow_page.default_flow_view = 'topiclist';
/**
* Create a new topic. 發新話題。 Reply to an existing topic.
*
* @param {String|Array}title
* page title 頁面標題。 {String}title or [ {String}API_URL,
* {String}title or {Object}page_data ]
* @param {String}topic
* 新話題的標題文字。 {String}topic
* @param {String|Function}text
* page contents 頁面內容。 {String}text or {Function}text(page_data)
* @param {Object}token
* login 資訊包含“csrf”令牌/密鑰。
* @param {Object}[options]
* 附加參數/設定選擇性/特殊功能與選項
* @param {Function}[callback]
* 回調函數。 callback(title, error, result)
*
* @see https://www.mediawiki.org/w/api.php?action=help&modules=flow%2Bnew-topic
* https://www.mediawiki.org/w/api.php?action=help&modules=flow%2Breply
*/
function edit_topic(title, topic, text, token, options, callback) {
// console.log(text);
if (library_namespace.is_thenable(text)) {
text.then(function(text) {
edit_topic(title, topic, text, token, options, callback);
}, function(error) {
callback(title, error);
});
return;
}
var action = 'action=flow';
// 處理 [ {String}API_URL, {String}title or {Object}page_data ]
if (Array.isArray(title)) {
action = [ title[0], action ];
title = title[1];
} else if (options[KEY_SESSION]) {
action = [ options[KEY_SESSION].API_URL, action ];
}
if (wiki_API.is_page_data(title))
title = title.title;
// assert: typeof title === 'string' or title is invalid.
if (title.length > 260) {
// [nttopic] 話題標題已限制在 260 位元組內。
// 自動評論與摘要的長度限制是260個字符。需要小心任何超出上述限定的東西將被裁剪掉。
// 260 characters
// https://github.com/wikimedia/mediawiki-extensions-Flow/blob/master/includes/Model/PostRevision.php
// const MAX_TOPIC_LENGTH = 260;
// https://github.com/wikimedia/mediawiki-extensions-Flow/blob/master/i18n/zh-hant.json
library_namespace
.warn('edit_topic: Title is too long and will be truncated: ['
+ error.code + ']');
title = title.slice(0, 260);
}
// default parameters
var _options = {
// notification_name : 'flow',
submodule : 'new-topic',
page : title,
nttopic : topic,
ntcontent : text,
ntformat : 'wikitext'
};
edit_topic.copy_keys.forEach(function(key) {
if (options[key])
_options[key] = options[key];
});
// the token should be sent as the last parameter.
_options.token = library_namespace.is_Object(token) ? token.csrftoken
: token;
wiki_API.query(action, typeof callback === 'function'
//
&& function(data) {
if (library_namespace.is_debug(2)
// .show_value() @ interact.DOM, application.debug
&& library_namespace.show_value)
library_namespace.show_value(data, 'edit_topic: data');
var error = data && data.error;
// 檢查伺服器回應是否有錯誤資訊。
if (error) {
library_namespace.error('edit_topic: ['
//
+ error.code + '] ' + error.info);
} else if (!(data = data.flow)
//
|| !(data = data['new-topic']) || data.status !== 'ok') {
// data = { flow: { 'new-topic': { status: 'ok',
// workflow: '', committed: {} } } }
error = 'edit_topic: Bad status ['
//
+ (data && data.status) + ']';
library_namespace.error(error);
}
if (typeof callback === 'function') {
// title.title === wiki_API.title_of(title)
callback(title.title, error, data);
}
}, _options, options);
}
/** {Array}欲 copy 至 Flow edit parameters 之 keys。 */
edit_topic.copy_keys = 'summary|bot|redirect|nocreate'.split(',');
// ------------------------------------------------------------------------
// export 導出.
// CeL.wiki.Flow.*
Object.assign(Flow_info, {
is_Flow : is_Flow,
page : Flow_page,
edit : edit_topic
});
return Flow_info;
}

75
app/node_modules/cejs/application/net/wiki/README.wiki generated vendored Normal file
View File

@@ -0,0 +1,75 @@
<!-- The first line is blank due to BOM -->
= CeJS MediaWiki module =
MediaWiki 自動化作業程式庫,主要用於編寫[[維基百科:機器人]]。
== Usage in [https://www.mediawiki.org/wiki/Manual:Interface/JavaScript mediawiki user script] (User:Example/common.js) ==
<pre><code>
if (!window.CeL) {
window.CeL = { initializer: function() { CeL.run('application.net.wiki', CeL_initialization); } };
mw.loader.load('https://kanasimi.github.io/CeJS/ce.js');
}
function CeL_initialization() {
/** {Array} parsed page content */
const parsed = CeL.wiki.parser('{{tl|t}}');
parsed.each('template', function(token) { console.log(token.name); });
const wiki = CeL.wiki.mw_web_session;
// wiki.page('Wikipedia:Sandbox').edit(function(page_data) { return CeL.wiki.content_of(page_data) + '\ntest'; });
}
</code></pre>
(At 2021, The JavaScript parser of MediaWiki loader cannot read ECMAScript 2016 syntax.)
Also refer to [https://kanasimi.github.io/CeJS/_test%20suite/wikitext_parser.html the wikitext parser examples].
; Append parameter to template:
<pre><code>
const wiki = CeL.wiki.mw_web_session;
wiki.page(mw.config.get('wgPageName')).edit(function(page_data) {
/** {Array} parsed page content */
const parsed = CeL.wiki.parser(page_data).parse();
parsed.each('Template:Artwork', function(token) {
token.push('wikidata=Q27964733');
});
return parsed.toString();
}, {
summary: 'test edit'
}).run(function() {
location.reload();
});
</code></pre>
== Operation mechanism 運作機制 ==
Main initial point: [[../wiki.js]]
; Essential 必要: [[../wiki.js]] → [[namespace.js]] → [[parser.js]], [[query.js]], [[page.js]], [[Flow.js]], [[list.js]], [[edit.js]], [[task.js]]
; Optional 可選功能: [[data.js]], [[admin.js]], [[cache.js]], [[Toolforge.js]]
; Change with wikiproject page contents 隨各 wikiproject 頁面內容變化之功能: [[template_functions.js]], [[featured_content.js]]
More examples: 使用範例可參照:
<!--
const util = require('util'); new util.promisify(CeL.wiki)(...)
-->
* [https://github.com/kanasimi/wikiapi JavaScript MediaWiki API for ECMAScript 2017+] / [https://github.com/kanasimi/wikiapi/blob/master/wikiapi.js wikiapi.js]
* [https://github.com/kanasimi/wikibot Wikipedia bots demo] / [https://github.com/kanasimi/wikibot/blob/master/wiki%20loader.js wiki loader.js]
* [[/_test suite/test.js|test.js]]
* [https://kanasimi.github.io/CeJS/_test%20suite/wikitext_parser.html Wikitext parser examples. Wikitext 解析器使用例子]
== History ==
{| class="wikitable"
|+ History 沿革
! Date !! Modify
|-
| 2015/1/1 || Starting to write codes.
開始撰寫模組程式碼。
|-
| 2019/10/11 || 分拆至 wiki/*.js
|-
| 2020/5/24 || 分拆 wiki.js。基本功能僅需要 `CeL.run('application.net.wiki')`。
|}
== See also ==
* [https://www.mediawiki.org/w/api.php MediaWiki API help]

902
app/node_modules/cejs/application/net/wiki/Toolforge.js generated vendored Normal file
View File

@@ -0,0 +1,902 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): Toolforge only functions
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* 條件合適時,應該會由 CeL.application.net.wiki 載入。
*
* TODO:<code>
</code>
*
* @since 2019/10/11 拆分自 CeL.application.net.wiki
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.Toolforge',
require : 'data.native.|application.storage.' + '|application.net.wiki.'
// load MediaWiki module basic functions
+ '|application.net.wiki.namespace.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki, KEY_SESSION = wiki_API.KEY_SESSION;
// ------------------------------------------------------------------------
// SQL 相關函數 @ Toolforge。
var
/** {String}user home directory */
home_directory = library_namespace.env.home,
/** {String}Wikimedia Toolforge database host */
TOOLSDB = 'tools-db',
/** {String}user/bot name */
user_name,
/** {String}Wikimedia Toolforge name. CeL.wiki.wmflabs */
wmflabs = wiki_API.wmflabs,
/** {Object}Wikimedia Toolforge job data. CeL.wiki.job_data */
job_data,
/** node mysql handler */
node_mysql,
/** {Object}default SQL configurations */
SQL_config;
if (home_directory
&& (home_directory = home_directory.replace(/[\\\/]$/, '').trim())) {
user_name = home_directory.match(/[^\\\/]+$/);
user_name = user_name ? user_name[0] : undefined;
if (user_name) {
wiki_API.user_name = user_name;
}
// There is no CeL.storage.append_path_separator() here!
home_directory += library_namespace.env.path_separator;
}
// setup SQL config language (and database/host).
function set_SQL_config_language(language) {
if (!language) {
return;
}
if (typeof language !== 'string') {
library_namespace.error(
//
'set_SQL_config_language: Invalid language: [' + language + ']');
return;
}
if (language === TOOLSDB) {
this.host = language;
// delete this.database;
return;
}
// 正規化。
var site = wiki_API.site_name(language);
// TODO: 'zh.news'
// 警告: this.language 可能包含 'zhwikinews' 之類。
this.host = site + set_SQL_config_language.hostname_postfix;
/**
* The database names themselves consist of the mediawiki project name,
* suffixed with _p
*
* @see https://wikitech.wikimedia.org/wiki/Help:Toolforge/Database
*/
this.database = site + '_p';
// console.log(this);
}
// https://wikitech.wikimedia.org/wiki/Help:Toolforge/Database#Connecting_to_the_database_replicas
// .analytics.db.svc.wikimedia.cloud
// @seealso https://phabricator.wikimedia.org/T142807
set_SQL_config_language.hostname_postfix = '.web.db.svc.wikimedia.cloud';
/**
* return new SQL config
*
* @param {String}[language]
* database language.<br />
* e.g., 'en', 'commons', 'wikidata', 'meta'.
* @param {String}[user]
* SQL database user name
* @param {String}[password]
* SQL database user password
*
* @returns {Object}SQL config
*/
function new_SQL_config(language, user, password) {
var config, is_clone;
if (user) {
config = {
user : user,
password : password,
db_prefix : user + '__',
set_language : set_SQL_config_language
};
} else if (SQL_config) {
is_clone = true;
config = Object.clone(SQL_config);
} else {
config = {};
}
if (typeof language === 'object') {
if (is_clone) {
delete config.database;
}
if (language.API_URL) {
// treat language as session.
// use set_SQL_config_language()
config.set_language(wiki_API.site_name(language), !user);
} else {
Object.assign(config, language);
}
} else if (typeof language === 'string' && language) {
if (is_clone) {
delete config.database;
}
// change language (and database/host).
config.set_language(language, !user);
}
return config;
}
/**
* 讀取並解析出 SQL 設定。
*
* @param {String}file_name
* file name
*
* @returns {Object}SQL config
*/
function parse_SQL_config(file_name) {
var config;
try {
config = library_namespace.get_file(file_name);
} catch (e) {
library_namespace.error(
//
'parse_SQL_config: Cannot read config file [ ' + file_name + ']!');
return;
}
// 應該用 parser。
var user = config.match(/\n\s*user\s*=\s*(\S+)/), password;
if (!user || !(password = config.match(/\n\s*password\s*=\s*(\S+)/)))
return;
return new_SQL_config(wiki_API.language, user[1], password[1]);
}
if (wmflabs) {
try {
node_mysql = require('mysql');
if (node_mysql) {
SQL_config = parse_SQL_config(home_directory
// The production replicas.
// https://wikitech.wikimedia.org/wiki/Help:Toolforge#The_databases
// https://wikitech.wikimedia.org/wiki/Help:Toolforge/Database
// Wikimedia Toolforge
// 上之資料庫僅為正式上線版之刪節副本。資料並非最新版本(但誤差多於數分內),也不完全,
// <del>甚至可能為其他 users 竄改過</del>。
+ 'replica.my.cnf');
}
} catch (e) {
library_namespace.error(e);
}
if (process.env.JOB_ID && process.env.JOB_NAME) {
// assert: process.env.ENVIRONMENT === 'BATCH'
wiki_API.job_data = job_data = {
id : process.env.JOB_ID,
name : process.env.JOB_NAME,
request : process.env.REQUEST,
script : process.env.JOB_SCRIPT,
stdout_file : process.env.SGE_STDOUT_PATH,
stderr_file : process.env.SGE_STDERR_PATH,
// 'continuous' or 'task'
is_task : process.env.QUEUE === 'task'
};
}
}
// ------------------------------------------------------------------------
/**
* execute SQL command.
*
* @param {String}SQL
* SQL command.
* @param {Function}callback
* 回調函數。 callback({Object}error, {Array}rows, {Array}fields)
* @param {Object}[config]
* configuration.
*
* @see https://wikitech.wikimedia.org/wiki/Help:Toolforge/Database
*
* @require https://github.com/mysqljs/mysql <br />
* https://quarry.wmflabs.org/ <br />
* TODO: https://github.com/sidorares/node-mysql2
*/
function run_SQL(SQL, callback, config) {
var _callback = function(error, results, fields) {
// the connection will return to the pool, ready to be used again by
// someone else.
// connection.release();
// close the connection and remove it from the pool
// connection.destroy();
callback(error, results, fields);
};
_callback = callback;
// TypeError: Converting circular structure to JSON
// library_namespace.debug(JSON.stringify(config), 3, 'run_SQL');
if (!config && !(config = SQL_config)) {
return;
}
// treat config as language.
if (typeof config === 'string' || wiki_API.is_wiki_API(config)) {
config = new_SQL_config(config);
}
library_namespace.debug(String(SQL), 3, 'run_SQL');
// console.log(JSON.stringify(config));
var connection = node_mysql.createConnection(config);
connection.connect();
if (Array.isArray(SQL)) {
// ("SQL", [values], callback)
connection.query(SQL[0], SQL[1], _callback);
} else {
// ("SQL", callback)
connection.query(SQL, _callback);
}
connection.end();
}
if (false) {
CeL.wiki.SQL('SELECT * FROM `revision` LIMIT 3000,1;',
//
function(error, rows, fields) {
if (error)
throw error;
// console.log('The result is:');
console.log(rows);
});
}
// ------------------------------------------------------------------------
/**
* Create a new user database.
*
* @param {String}dbname
* database name.
* @param {Function}callback
* 回調函數。
* @param {String}[language]
* database language.<br />
* e.g., 'en', 'commons', 'wikidata', 'meta'.
*
* @see https://wikitech.wikimedia.org/wiki/Help:Tool_Labs/Database#Creating_new_databases
*/
function create_database(dbname, callback, language) {
if (!SQL_config)
return;
var config;
if (typeof dbname === 'object') {
config = Object.clone(dbname);
dbname = config.database;
delete config.database;
} else {
config = new_SQL_config(language || TOOLSDB);
if (!language) {
delete config.database;
}
}
library_namespace.log('create_database: Try to create database ['
+ dbname + ']');
if (false) {
/**
* 用此方法會:<br />
* [Error: ER_PARSE_ERROR: You have an error in your SQL syntax;
* check the manual that corresponds to your MariaDB server version
* for the right syntax to use near ''user__db'' at line 1]
*/
var SQL = {
// placeholder 佔位符
// 避免 error.code === 'ER_DB_CREATE_EXISTS'
sql : 'CREATE DATABASE IF NOT EXISTS ?',
values : [ dbname ]
};
}
if (dbname.includes('`'))
throw new Error('Invalid database name: [' + dbname + ']');
run_SQL('CREATE DATABASE IF NOT EXISTS `' + dbname + '`', function(
error, rows, fields) {
if (typeof callback !== 'function')
return;
if (error)
callback(error);
else
callback(null, rows, fields);
}, config);
return config;
}
// ------------------------------------------------------------------------
/**
* SQL 查詢功能之前端。
*
* @example <code>
// change language (and database/host).
//CeL.wiki.SQL.config.set_language('en');
CeL.wiki.SQL(SQL, function callback(error, rows, fields) { if(error) console.error(error); else console.log(rows); }, 'en');
// get sitelink count of wikidata items
// https://www.mediawiki.org/wiki/Wikibase/Schema/wb_items_per_site
// https://www.wikidata.org/w/api.php?action=help&modules=wbsetsitelink
var SQL_get_sitelink_count = 'SELECT ips_item_id, COUNT(*) AS `link_count` FROM wb_items_per_site GROUP BY ips_item_id LIMIT 10';
var SQL_session = new CeL.wiki.SQL(function(error){}, 'wikidata');
function callback(error, rows, fields) { if(error) console.error(error); else console.log(rows); SQL_session.connection.destroy(); }
SQL_session.SQL(SQL_get_sitelink_count, callback);
// one-time method
CeL.wiki.SQL(SQL_get_sitelink_count, callback, 'wikidata');
* </code>
*
* @example <code>
// 進入 default host (TOOLSDB)。
var SQL_session = new CeL.wiki.SQL(()=>{});
// 進入 default host (TOOLSDB),並預先創建 user's database 'dbname' (e.g., 's00000__dbname')
var SQL_session = new CeL.wiki.SQL('dbname', ()=>{});
// 進入 zhwiki.zhwiki_p。
var SQL_session = new CeL.wiki.SQL(()=>{}, 'zh');
// 進入 zhwiki.zhwiki_p並預先創建 user's database 'dbname' (e.g., 's00000__dbname')
var SQL_session = new CeL.wiki.SQL('dbname', ()=>{}, 'zh');
// create {SQL_session}instance
new CeL.wiki.SQL('mydb', function callback(error, rows, fields) { if(error) console.error(error); } )
// run SQL query
.SQL(SQL, function callback(error, rows, fields) { if(error) console.error(error); } );
SQL_session.connection.destroy();
* </code>
*
* @param {String}[dbname]
* database name.
* @param {Function}callback
* 回調函數。 callback(error)
* @param {String}[language]
* database language (and database/host). default host: TOOLSDB.<br />
* e.g., 'en', 'commons', 'wikidata', 'meta'.
*
* @returns {SQL_session}instance
*
* @constructor
*/
function SQL_session(dbname, callback, language) {
if (!(this instanceof SQL_session)) {
if (typeof language === 'object') {
language = new_SQL_config(language);
} else if (typeof language === 'string' && language) {
// change language (and database/host).
SQL_config.set_language(language);
if (language === TOOLSDB)
delete SQL_config.database;
language = null;
}
// dbname as SQL query string.
return run_SQL(dbname, callback, language);
}
if (typeof dbname === 'function' && !language) {
// shift arguments
language = callback;
callback = dbname;
dbname = null;
}
this.config = new_SQL_config(language || TOOLSDB);
if (dbname) {
if (typeof dbname === 'object') {
Object.assign(this.config, dbname);
} else {
// 自動添加 prefix。
this.config.database = this.config.db_prefix + dbname;
}
} else if (this.config.host === TOOLSDB) {
delete this.config.database;
} else {
// this.config.database 已經在 set_SQL_config_language() 設定。
}
var _this = this;
this.connect(function(error) {
// console.error(error);
if (error && error.code === 'ER_BAD_DB_ERROR'
&& !_this.config.no_create && _this.config.database) {
// Error: ER_BAD_DB_ERROR: Unknown database '...'
create_database(_this.config, callback);
} else if (typeof callback === 'function') {
callback(error);
}
});
}
// need reset connection,
function need_reconnect(error) {
return error
// Error: Cannot enqueue Handshake after fatal error.
&& (error.code === 'PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR'
// ECONNRESET: socket hang up
|| error.code === 'ECONNRESET');
}
// run SQL query
SQL_session.prototype.SQL = function(SQL, callback) {
var _this = this;
this.connection.query(SQL, function(error) {
if (need_reconnect(error)) {
// re-connect. 可能已經斷線。
_this.connection.connect(function(error) {
if (error) {
// console.error(error);
}
_this.connection.query(SQL, callback);
});
} else {
callback.apply(null, arguments);
}
});
return this;
};
SQL_session.prototype.connect = function(callback, force) {
if (!force)
try {
var _this = this;
this.connection.connect(function(error) {
if (need_reconnect(error)) {
// re-connect.
_this.connect(callback, true);
} else if (typeof callback === 'function')
callback(error);
});
return this;
} catch (e) {
// TODO: handle exception
}
try {
this.connection.end();
} catch (e) {
// TODO: handle exception
}
// 需要重新設定 this.connection否則會出現:
// Error: Cannot enqueue Handshake after invoking quit.
this.connection = node_mysql.createConnection(this.config);
this.connection.connect(callback);
return this;
};
/**
* get database list.
*
* <code>
var SQL_session = new CeL.wiki.SQL('testdb',
//
function callback(error, rows, fields) {
if (error)
console.error(error);
else
s.databases(function(list) {
console.log(list);
});
});
</code>
*
* @param {Function}callback
* 回調函數。
* @param {Boolean}all
* get all databases. else: get my databases.
*
* @returns {SQL_session}
*/
SQL_session.prototype.databases = function(callback, all) {
var _this = this;
function filter(dbname) {
return dbname.startsWith(_this.config.db_prefix);
}
if (this.database_cache) {
var list = this.database_cache;
if (!all)
// .filter() 會失去 array 之其他屬性。
list = list.filter(filter);
if (typeof callback === 'function')
callback(list);
return this;
}
var SQL = 'SHOW DATABASES';
if (false && !all)
// SHOW DATABASES LIKE 'pattern';
SQL += " LIKE '" + this.config.db_prefix + "%'";
this.connect(function(error) {
// reset connection,
// 預防 PROTOCOL_ENQUEUE_AFTER_FATAL_ERROR
_this.connection.query(SQL, function(error, rows, fields) {
if (error || !Array.isArray(rows)) {
library_namespace.error(error);
rows = null;
} else {
rows = rows.map(function(row) {
for ( var field in row)
return row[field];
});
_this.database_cache = rows;
if (!all)
// .filter() 會失去 array 之其他屬性。
rows = rows.filter(filter);
// console.log(rows);
}
if (typeof callback === 'function')
callback(rows);
});
});
return this;
};
if (SQL_config) {
library_namespace
.debug('wiki_API.SQL_session: You may use SQL to get data.');
wiki_API.SQL = SQL_session;
// export 導出: CeL.wiki.SQL() 僅可在 Wikimedia Toolforge 上使用。
wiki_API.SQL.config = SQL_config;
// wiki_API.SQL.create = create_database;
}
// ----------------------------------------------------
/**
* Convert MediaWiki database timestamp to ISO 8601 format.<br />
* UTC: 'yyyymmddhhmmss' → 'yyyy-mm-ddThh:mm:ss'
*
* @param {String|Buffer}timestamp
* MediaWiki database timestamp
*
* @returns {String}ISO 8601 Data elements and interchange formats
*
* @see https://www.mediawiki.org/wiki/Manual:Timestamp
*/
function SQL_timestamp_to_ISO(timestamp) {
if (!timestamp) {
// ''?
return;
}
// timestamp可能為{Buffer}
timestamp = timestamp.toString('utf8').chunk(2);
if (timestamp.length !== 7) {
// 'NULL'?
return;
}
return timestamp[0] + timestamp[1]
//
+ '-' + timestamp[2] + '-' + timestamp[3]
//
+ 'T' + timestamp[4] + ':' + timestamp[5] + ':' + timestamp[6] + 'Z';
}
function generate_SQL_WHERE(condition, field_prefix) {
var condition_array = [], value_array = [];
if (typeof condition === 'string') {
;
} else if (Array.isArray(condition)) {
// TODO: for ' OR '
condition = condition.join(' AND ');
} else if (library_namespace.is_Object(condition)) {
for ( var name in condition) {
var value = condition[name];
if (value === undefined) {
// 跳過這一筆設定。
continue;
}
if (!name) {
// condition[''] = [ condition 1, condition 2, ...];
if (Array.isArray(value)) {
value_array.append(value);
} else {
value_array.push(value);
}
continue;
}
if (!/^[a-z_]+$/.test(name)) {
throw 'Invalid field name: ' + name;
}
if (!name.startsWith(field_prefix)) {
name = field_prefix + name;
}
var matched = typeof value === 'string'
// TODO: for other operators
// @see https://mariadb.com/kb/en/mariadb/select/
// https://mariadb.com/kb/en/mariadb/functions-and-operators/
&& value.match(/^([<>!]?=|[<>]|<=>|IN |IS )([\s\S]+)$/);
if (matched) {
name += matched[1] + '?';
// DO NOT quote the value yourself!!
value = matched[2];
// Number.MAX_SAFE_INTEGER starts from 9.
if (/^[+\-]?[1-9]\d{0,15}$/.test(value)
// ↑ 15 = String(Number.MAX_SAFE_INTEGER).length-1
&& +value <= Number.MAX_SAFE_INTEGER) {
value = +value;
}
} else {
name += '=?';
}
condition_array.push(name);
value_array.push(value);
}
// TODO: for ' OR '
condition = condition_array.join(' AND ');
} else {
library_namespace.error('Invalid condition: '
+ JSON.stringify(condition));
return;
}
return [ condition ? ' WHERE ' + condition : '', value_array ];
}
// ----------------------------------------------------
// https://www.mediawiki.org/wiki/API:RecentChanges
// const
var ENUM_rc_type = 'edit,new,move,log,move over redirect,external,categorize';
/**
* Get page title 頁面標題 list of [[Special:RecentChanges]] 最近更改.
*
* @examples<code>
// get title list
CeL.wiki.recent(function(rows){console.log(rows.map(function(row){return row.title;}));}, {language:'ja', namespace:0, limit:20});
// 應並用 timestamp + this_oldid
CeL.wiki.recent(function(rows){console.log(rows.map(function(row){return [row.title,row.rev_id,row.row.rc_timestamp.toString()];}));}, {where:{timestamp:'>=20170327143435',this_oldid:'>'+43772537}});
</code>
*
* TODO: filter
*
* @param {Function}callback
* 回調函數。 callback({Array}page title 頁面標題 list)
* @param {Object}[options]
* 附加參數/設定選擇性/特殊功能與選項。
*
* @see https://www.mediawiki.org/wiki/Manual:Recentchanges_table
* https://www.mediawiki.org/wiki/Actor_migration
*/
function get_recent_via_databases(callback, options) {
if (options && (typeof options === 'string')) {
options = {
// treat options as language
language : options
};
} else {
options = library_namespace.setup_options(options);
}
// console.trace(options);
var SQL = options.SQL;
if (!SQL) {
SQL = Object.create(null);
if (options.bot === 0 || options.bot === 1) {
// assert: 0 || 1
SQL.bot = options.bot;
}
// 不指定namespace或者指定namespace為((undefined)): 取得所有的namespace。
/** {Integer|String}namespace NO. */
var namespace = wiki_API.namespace(options.namespace);
if (namespace !== undefined) {
SQL.namespace = namespace;
}
Object.assign(SQL,
// {String|Array|Object}options.where: 自訂篩選條件。
options.where);
SQL = generate_SQL_WHERE(SQL, 'rc_');
// console.log(SQL);
// https://phabricator.wikimedia.org/T223406
// TODO: 舊版上 `actor`, `comment` 這兩個資料表不存在會出錯,需要先偵測。
// TODO: use JSON: https://phabricator.wikimedia.org/T299417
var fields = [
'*',
// https://www.mediawiki.org/wiki/Manual:Actor_table#actor_id
'(SELECT `actor_user` FROM `actor` WHERE `actor`.`actor_id` = `recentchanges`.`rc_actor`) AS `userid`',
'(SELECT `actor_name` FROM `actor` WHERE `actor`.`actor_id` = `recentchanges`.`rc_actor`) AS `user_name`',
// https://www.mediawiki.org/wiki/Manual:Comment_table#comment_id
'(SELECT `comment_text` FROM `comment` WHERE `comment`.`comment_id` = `recentchanges`.`rc_comment_id`) AS `comment`',
'(SELECT `comment_data` FROM `comment` WHERE `comment`.`comment_id` = `recentchanges`.`rc_comment_id`) AS `comment_data`' ];
SQL[0] = 'SELECT ' + fields.join(',')
// https://www.mediawiki.org/wiki/Manual:Recentchanges_table
+ ' FROM `recentchanges`' + SQL[0]
// new → old, may contain duplicate title.
// or `rc_timestamp`
// or rc_this_oldid, but too slow (no index).
// ASC: 小 → 大DESC: 大 → 小
+ ' ORDER BY `rc_this_oldid` ASC LIMIT ' + (
/** {ℕ⁰:Natural+0}limit count. */
options.limit > 0 ? Math.min(options.limit
// 筆數限制。就算隨意輸入,強制最多只能這麼多筆資料。
, 1e4)
// default records to get
: options.where ? 1e4 : 5000);
}
if (false) {
console.log([ options.config, options.language,
options[KEY_SESSION] && options[KEY_SESSION].language ]);
console.log(options[KEY_SESSION]);
throw 1;
}
run_SQL(SQL, function(error, rows, fields) {
if (error) {
callback();
return;
}
var result = [];
rows.forEach(function(row) {
if (!(row.rc_user > 0) && !(row.rc_type < 5)
//
&& (!('rc_type' in options)
//
|| options.rc_type !== ENUM_rc_type[row.rc_type])) {
// On wikis using Wikibase the results will otherwise be
// meaningless.
return;
}
var namespace_text = row.rc_namespace
// pass session = options[KEY_SESSION]
? wiki_API.namespace.name_of(row.rc_namespace, options) + ':'
: '';
// 基本上 API 盡可能模擬 recentchanges與之一致。
result.push({
type : ENUM_rc_type[row.rc_type],
// namespace
ns : row.rc_namespace,
// .rc_title 未加上 namespace prefix!
title : (namespace_text
// @see normalize_page_name()
+ row.rc_title.toString()).replace(/_/g, ' '),
// links to the page_id key in the page table
// 0: 可能為flow. 此時title為主頁面名非topic。由.rc_params可獲得相關資訊。
pageid : row.rc_cur_id,
// rev_id
// Links to the rev_id key of the new page revision
// (after the edit occurs) in the revision table.
revid : row.rc_this_oldid,
old_revid : row.rc_last_oldid,
rcid : row.rc_id,
user : row.user_name && row.user_name.toString()
// text of the username for the user that made the
// change, or the IP address if the change was made by
// an unregistered user. Corresponds to rev_user_text
//
// `rc_user_text` deprecated: MediaWiki version: ≤ 1.33
|| row.rc_user_text && row.rc_user_text.toString(),
// NULL for anonymous edits
userid : row.userid
// 0 for anonymous edits
// `rc_user` deprecated: MediaWiki version: ≤ 1.33
|| row.rc_user,
// old_length
oldlen : row.rc_old_len,
// new length
newlen : row.rc_new_len,
// Corresponds to rev_timestamp
// use new Date(.timestamp)
timestamp : SQL_timestamp_to_ISO(row.rc_timestamp),
comment : row.comment && row.comment.toString()
// `rc_comment` deprecated: MediaWiki version: ≤ 1.32
|| row.rc_comment && row.rc_comment.toString(),
// usually NULL
comment_data : row.comment_data
&& row.comment_data.toString(),
// parsedcomment : TODO,
logid : row.rc_logid,
// TODO
logtype : row.rc_log_type,
logaction : row.rc_log_action.toString(),
// logparams: TODO: should be {Object}, e.g., {userid:0}
logparams : row.rc_params.toString(),
// tags: ["TODO"],
// 以下為recentchanges之外本函數額外加入。
is_new : !!row.rc_new,
// e.g., 1 or 0
// is_bot : !!row.rc_bot,
// is_minor : !!row.rc_minor,
// e.g., mw.edit
is_Flow : row.rc_source.toString() === 'flow',
// patrolled : !!row.rc_patrolled,
// deleted : !!row.rc_deleted,
row : row
});
});
callback(result);
},
// SQL config
options.config || options.language || options[KEY_SESSION]);
}
// 可能會因環境而不同的功能。讓 wiki_API.recent 採用較有效率的實現方式。
if (SQL_config) {
wiki_API.recent =
// SQL_config ? get_recent_via_databases : get_recent_via_API;
get_recent_via_databases;
}
// ------------------------------------------------------------------------
// export 導出.
// @inner
library_namespace.set_method(wiki_API, {
SQL_config : SQL_config,
new_SQL_config : new_SQL_config,
run_SQL : run_SQL
});
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

372
app/node_modules/cejs/application/net/wiki/admin.js generated vendored Normal file
View File

@@ -0,0 +1,372 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): 管理員相關的 adminship
* functions
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
</code>
*
* @since 2019/10/12 拆分自 CeL.application.net.wiki
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.admin',
require : 'application.net.wiki.'
// load MediaWiki module basic functions
+ '|application.net.wiki.namespace.'
//
+ '|application.net.wiki.query.|application.net.wiki.page.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki, KEY_SESSION = wiki_API.KEY_SESSION;
// ------------------------------------------------------------------------
// administrator functions. 管理員相關函數。
// 自 options 汲取出 parameters。
// TODO: 整合進 normalize_parameters。
// default_parameters[parameter name] = required
function draw_parameters(options, default_parameters, token_type) {
if (!options) {
// Invalid options/parameters
return 'No options specified';
}
// 汲取出 parameters。
var parameters = Object.create(null);
if (default_parameters) {
for ( var parameter_name in default_parameters) {
if (parameter_name in options) {
// in case options[parameter_name] === false
if (options[parameter_name])
parameters[parameter_name] = options[parameter_name];
} else if (default_parameters[parameter_name]) {
// 表示此屬性為必須存在/指定的屬性。
// This parameter is required.
return 'No property ' + parameter_name + ' specified';
}
}
}
var session = options[KEY_SESSION];
// assert: 有parameters, e.g., {Object}parameters
// 可能沒有 session
// ----------------------------
// 處理 target page。
var default_KEY_ID = 'pageid', default_KEY_TITLE = 'title', KEY_ID = default_KEY_ID, KEY_TITLE = default_KEY_TITLE;
if (parameters.to) {
// move_to
KEY_ID = 'fromid';
KEY_TITLE = 'from';
}
// 都先從 options 取值,再從 session 取值。
if (options[KEY_ID] >= 0 || options[default_KEY_ID] >= 0) {
parameters[KEY_ID] = options[KEY_ID] >= 0 ? options[KEY_ID]
: options[default_KEY_ID];
} else if (options[KEY_TITLE] || options[default_KEY_TITLE]) {
parameters[KEY_TITLE] = wiki_API.title_of(options[KEY_TITLE]
// options.from_title
|| options[default_KEY_TITLE]);
} else if (wiki_API.is_page_data(session && session.last_page)) {
// options.page_data
if (session.last_page.pageid >= 0)
parameters[KEY_ID] = session.last_page.pageid;
else
parameters[KEY_TITLE] = session.last_page.title;
} else {
// 可能沒有 page_data
if (library_namespace.is_debug()) {
library_namespace.error('draw_parameters: No page specified: '
+ JSON.stringify(options));
}
return 'No page id/title specified';
}
// ----------------------------
// 處理 token。
if (!token_type) {
token_type = 'csrf';
}
var token = options.token || session && session.token;
if (token && typeof token === 'object') {
// session.token.csrftoken
token = token[token_type + 'token'];
}
if (!token) {
// TODO: use session
if (false) {
library_namespace.error('draw_parameters: No token specified: '
+ options);
}
return 'No ' + token_type + 'token specified';
}
parameters.token = token;
return parameters;
}
// use "csrf" token retrieved from action=query&meta=tokens
// callback(response, error);
function wiki_operator(action, default_parameters, options, callback) {
// default_parameters
// Warning: 除 pageid/title/token 之外,這邊只要是能指定給 API 的,皆必須列入!
var parameters = draw_parameters(options, default_parameters);
// console.log(parameters);
if (!library_namespace.is_Object(parameters)) {
// error occurred.
if (typeof callback === 'function')
callback(undefined, parameters);
return;
}
// TODO: 若是頁面不存在/已刪除,那就直接跳出。
if (action === 'move') {
library_namespace.is_debug((parameters.fromid || parameters.from)
// .move_to_title
+ ' → ' + parameters.to, 1, 'wiki_operator.move');
}
wiki_API.query({
action : action
}, function(response, error) {
// console.log(JSON.stringify(response));
if (wiki_API.query.handle_error(response, error, callback)) {
return;
}
callback(response[action]);
}, parameters, options);
}
// ================================================================================================================
// wiki_API.delete(): remove / delete a page.
wiki_API['delete'] = function(options, callback) {
// https://www.mediawiki.org/w/api.php?action=help&modules=delete
/**
* response: <code>
{"delete":{"title":"Title","reason":"content was: \"...\", and the only contributor was \"[[Special:Contributions/Cewbot|Cewbot]]\" ([[User talk:Cewbot|talk]])","logid":0000}}
{"error":{"code":"nosuchpageid","info":"There is no page with ID 0.","*":"See https://test.wikipedia.org/w/api.php for API usage. Subscribe to the mediawiki-api-announce mailing list at &lt;https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce&gt; for notice of API deprecations and breaking changes."},"servedby":"mw1232"}
* </code>
*/
wiki_operator('delete', {
reason : false,
tags : false,
watchlist : false,
watchlistexpiry : false,
oldimage : false
}, options, callback);
};
// ----------------------------------------------------
// wiki_API.move_to(): move a page from `from` to target `to`.
wiki_API.move_to = function(options, callback) {
// https://www.mediawiki.org/w/api.php?action=help&modules=move
var default_parameters = {
to : true,
reason : false,
movetalk : false,
movesubpages : false,
noredirect : false,
watchlist : false,
ignorewarnings : false,
tags : false
};
if (!options || !options.reason) {
library_namespace
.warn('wiki_API.move_to: Should set reason when moving page!');
}
/**
* response: <code>
{"error":{"code":"nosuchpageid","info":"There is no page with ID 0.","*":"See https://zh.wikipedia.org/w/api.php for API usage. Subscribe to the mediawiki-api-announce mailing list at &lt;https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce&gt; for notice of API deprecations and breaking changes."},"servedby":"mw1277"}
error:
{"code":"articleexists","info":"A page of that name already exists, or the name you have chosen is not valid. Please choose another name.","*":"See https://zh.wikipedia.org/w/api.php for API usage. Subscribe to the mediawiki-api-announce mailing list at &lt;https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce&gt; for notice of API deprecations and breaking changes."}
{"code":"selfmove","info":"The title is the same; cannot move a page over itself.","*":"See https://zh.wikipedia.org/w/api.php for API usage. Subscribe to the mediawiki-api-announce mailing list at &lt;https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce&gt; for notice of API deprecations and breaking changes."}
{ from: 'from', to: 'to', reason: '', redirectcreated: '', moveoverredirect: '' }
{ error: { code: 'articleexists', info: 'A page already exists at [[:To]], or the page name you have chosen is not valid. Please choose another name.', '*': 'See https://test.wikipedia.org/w/api.php for API usage. Subscribe to the mediawiki-api-announce mailing list at &lt;https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce&gt; for notice of API deprecations and breaking changes.' } }
</code>
*/
// console.log(options);
wiki_operator('move', default_parameters, options, callback);
};
// ----------------------------------------------------
// @see wiki_API.is_protected
// Change the protection level of a page.
wiki_API.protect = function(options, callback) {
// https://www.mediawiki.org/w/api.php?action=help&modules=protect
/**
* response: <code>
{"protect":{"title":"title","reason":"存檔保護作業","protections":[{"edit":"sysop","expiry":"infinite"},{"move":"sysop","expiry":"infinite"}]}}
{"servedby":"mw1203","error":{"code":"nosuchpageid","info":"There is no page with ID 2006","*":"See https://zh.wikinews.org/w/api.php for API usage"}}
* </code>
*/
wiki_operator('protect', {
// protections: e.g., 'edit=sysop|move=sysop', 一般說來edit應與move同步。
protections : true,
// 在正式場合,最好給個好的理由。
// reason: @see [[MediaWiki:Protect-dropdown]]
reason : false,
// expiry : 'infinite',
expiry : false,
tags : false,
cascade : false,
watchlist : false
}, Object.assign({
protections : 'edit=sysop|move=sysop'
}, options), callback);
};
// ----------------------------------------------------
// rollback 僅能快速撤銷/回退/還原某一頁面最新版本之作者(最後一位使用者)一系列所有編輯至另一作者的編輯
// The rollback revision will be marked as minor.
wiki_API.rollback = function(options, callback) {
var session = wiki_API.session_of_options(options);
if (session && !session.token.rollbacktoken) {
session.get_token(function() {
wiki_API.rollback(options, callback);
}, 'rollback');
}
var parameters = draw_parameters(options, {
// default_parameters
// Warning: 除外pageid/title/token這邊只要是能指定給 API 的,皆必須列入!
user : false,
summary : false,
markbot : false,
tags : false
}, 'rollback');
if (!library_namespace.is_Object(parameters)) {
// error occurred.
if (typeof callback === 'function')
callback(undefined, parameters);
return;
}
// 都先從 options 取值,再從 session 取值。
var page_data =
// options.page_data ||
options.pageid && options || session && session.last_page;
// assert: 有parameters, e.g., {Object}parameters
// 可能沒有 session, page_data
if (!parameters.user && wiki_API.content_of.revision(page_data)) {
// 將最後一個版本的編輯者當作回退對象。
parameters.user = wiki_API.content_of.revision(page_data).user;
}
// https://www.mediawiki.org/w/api.php?action=help&modules=rollback
// If the last user who edited the page made multiple edits in a row,
// they will all be rolled back.
if (!parameters.user) {
// 抓最後的編輯者試試。
// 要用pageid的話得採page_data就必須保證兩者相同。
if (!parameters.title && page_data
&& parameters.pageid !== page_data.pageid) {
callback(undefined, 'parameters.pageid !== page_data.pageid');
return;
}
wiki_API.page(page_data || parameters.title, function(page_data,
error) {
if (error || !wiki_API.content_of.revision(page_data)
// 保證不會再持續執行。
|| !wiki_API.content_of.revision(page_data).user) {
if (false) {
library_namespace.error(
//
'wiki_API.rollback: No user name specified!');
}
callback(undefined,
//
'No user name specified and I cannot guess it!');
return;
}
wiki_API.rollback(options, callback);
}, Object.assign({
rvprop : 'ids|timestamp|user'
}, options));
return;
}
if (!('markbot' in parameters) && options.bot) {
parameters.markbot = options.bot;
}
/**
* response: <code>
{"rollback":{"title":"title","pageid":1,"summary":"","revid":9,"old_revid":7,"last_revid":1,"messageHtml":"<p></p>"}}
{"servedby":"mw1190","error":{"code":"badtoken","info":"Invalid token","*":"See https://zh.wikinews.org/w/api.php for API usage"}}
* </code>
*/
wiki_API.query({
action : 'rollback'
}, function(response) {
var error = response && response.error;
if (error) {
callback(response, error);
} else {
// revid 回滾的版本ID。
// old_revid 被回滾的第一個最新修訂的修訂ID。
// last_revid 被回滾最後一個最舊版本的修訂ID。
// 如果回滾不會改變的頁面沒有新修訂而成。在這種情況下revid將等於old_revid。
callback(response.rollback);
}
}, parameters, options);
};
// ----------------------------------------------------
// 目前的修訂,不可隱藏。
// This is the current revision. It cannot be hidden.
wiki_API.hide = function(options, callback) {
TODO;
};
// ------------------------------------------------------------------------
// export 導出.
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

812
app/node_modules/cejs/application/net/wiki/cache.js generated vendored Normal file
View File

@@ -0,0 +1,812 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): cache
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
</code>
*
* @since 2020/5/24 6:21:13 拆分自 CeL.application.net.wiki
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.cache',
require : 'data.native.'
// for library_namespace.get_URL
+ '|application.net.Ajax.' + '|application.net.wiki.'
// load MediaWiki module basic functions
+ '|application.net.wiki.namespace.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki, KEY_SESSION = wiki_API.KEY_SESSION;
// --------------------------------------------------------------------------------------------
/** {Object|Function}fs in node.js */
var node_fs;
try {
if (library_namespace.platform.nodejs)
// @see https://nodejs.org/api/fs.html
node_fs = require('fs');
if (typeof node_fs.readFile !== 'function')
throw true;
} catch (e) {
// enumerate for wiki_API.cache
// 模擬 node.js 之 fs以達成最起碼的效果即無 cache 功能的情況)。
library_namespace.warn(this.id
+ ': 無 node.js 之 fs因此不具備 cache 或 SQL 功能。');
node_fs = {
// library_namespace.storage.read_file()
readFile : function(file_path, options, callback) {
library_namespace.error('Cannot read file ' + file_path);
if (typeof callback === 'function')
callback(true);
},
// library_namespace.storage.write_file()
writeFile : function(file_path, data, options, callback) {
library_namespace.error('Cannot write to file ' + file_path);
if (typeof options === 'function' && !callback)
callback = options;
if (typeof callback === 'function')
callback(true);
}
};
}
// --------------------------------------------------------------------------------------------
/**
* cache 相關函數:
*
* @see application.storage.file.get_cache_file
* application.OS.Windows.file.cacher
* application.net.Ajax.get_URL_cache<br />
* application.net.wiki<br />
* wiki_API.cache() CeL.wiki.cache()
*/
if (false) {
// examples
CeL.wiki.cache({
type : 'page',
file_name : 'file_name',
list : 'WP:SB',
operator : function(data) {
console.log(data);
}
}, function callback(data) {
console.log(data);
}, {
// default options === this
// namespace : '0|1',
// [KEY_SESSION]
// session : wiki,
// title_prefix : 'Template:',
// cache path prefix
prefix : 'base_directory/'
});
CeL.set_debug(6);
CeL.wiki.cache({
type : 'callback',
file_name : 'file_name',
list : function(callback) {
callback([ 1, 2, 3 ]);
},
operator : function(data) {
console.log(data);
}
}, function callback(data) {
console.log(data);
}, {
// default options === this
// namespace : '0|1',
// [KEY_SESSION]
// session : wiki,
// title_prefix : 'Template:',
// cache path prefix
prefix : './'
});
CeL.set_debug(6);
var wiki = Wiki(true);
CeL.wiki.cache({
type : 'wdq',
file_name : 'countries',
list : 'claim[31:6256]',
operator : function(list) {
// console.log(list);
result = list;
}
}, function callback(list) {
// console.log(list);
}, {
// default options === this
// namespace : '0|1',
// [KEY_SESSION]
session : wiki,
// title_prefix : 'Template:',
// cache path prefix
prefix : './'
});
}
/**
* cache 作業操作之輔助套裝函數。
*
* 注意: only for node.js. 必須自行 include 'application.platform.nodejs'。 <code>
CeL.run('application.platform.nodejs');
* </code><br />
* 注意: 需要自行先創建各 type 之次目錄,如 page, redirects, embeddedin, ...<br />
* 注意: 會改變 operation, _this Warning: will modify operation, _this!
*
* 連續作業: 依照 _this 設定 {Object}default options即傳遞於各 operator 間的 ((this))。<br />
* 依照 operation 順序個別執行單一項作業。
*
* 單一項作業流程:<br />
* 設定檔名。<br />
* 若不存在此檔,則:<br />
* >>> 依照 operation.type 與 operation.list 取得資料。<br />
* >>> 若 Array.isArray(operation.list) 則處理多項列表作業:<br />
* >>>>>> 個別處理單一項作業,每次執行 operation.each() || operation.each_retrieve()。<br />
* >>> 執行 data = operation.retrieve(data),以其回傳作為將要 cache 之 data。<br />
* >>> 寫入cache。<br />
* 執行 operation.operator(data)
*
* TODO: file_stream<br />
* TODO: do not write file
*
* @param {Object|Array}operation
* 作業設定。
* @param {Function}[callback]
* 所有作業(operation)執行完後之回調函數。 callback(response data)
* @param {Object}[_this]
* 傳遞於各 operator 間的 ((this))。注意: 會被本函數更動!
*/
function wiki_API_cache(operation, callback, _this) {
if (library_namespace.is_Object(callback) && !_this) {
// 未設定/不設定 callback
// shift arguments.
_this = callback;
callback = undefined;
}
var index = 0;
/**
* 連續作業時,轉到下一作業。
*
* node.js v0.11.16: In strict mode code, functions can only be declared
* at top level or immediately within another function.
*/
function next_operator(data) {
library_namespace.debug('處理連續作業序列,轉到下一作業: ' + (index + 1) + '/'
+ operation.length, 2, 'wiki_API_cache.next_operator');
// [ {Object}operation, {Object}operation, ... ]
// operation = { type:'embeddedin', operator:function(data) }
if (index < operation.length) {
var this_operation = operation[index++];
// console.log(this_operation);
if (!this_operation) {
// Allow null operation.
library_namespace.debug('未設定 operation[' + (index - 1)
+ ']。Skip this operation.', 1,
'wiki_API_cache.next_operator');
next_operator(data);
} else {
if (!('list' in this_operation)) {
// use previous data as list.
library_namespace.debug(
'未特別指定 list以前一次之回傳 data 作為 list。', 3,
'wiki_API_cache.next_operator');
library_namespace.debug('前一次之回傳 data: '
+ (data && JSON.stringify(data).slice(0, 180))
+ '...', 3, 'wiki_API_cache.next_operator');
this_operation.list = data;
}
if (data) {
library_namespace.debug('設定 .last_data_got: '
+ (data && JSON.stringify(data).slice(0, 180))
+ '...', 3, 'wiki_API_cache.next_operator');
this_operation.last_data_got = data;
}
// default options === _this: 傳遞於各 operator 間的 ((this))。
wiki_API_cache(this_operation, next_operator, _this);
}
} else if (typeof callback === 'function') {
if (false && Array.isArray(data)) {
// TODO: adapt to {Object}operation
library_namespace.log('wiki_API_cache: Get ' + data.length
+ ' page(s).');
// 自訂list
// data = [ '' ];
if (_this.limit >= 0) {
// 設定此初始值,可跳過之前已經處理過的。
data = data.slice(0 * _this.limit, 1 * _this.limit);
}
library_namespace.debug(data.slice(0, 8).map(
wiki_API.title_of).join('\n')
+ '\n...');
}
// last 收尾
callback.call(_this, data);
}
}
if (Array.isArray(operation)) {
next_operator();
return;
}
// ----------------------------------------------------
/**
* 以下為處理單一次作業。
*/
library_namespace.debug('處理單一次作業。', 2, 'wiki_API_cache');
library_namespace.debug(
'using operation: ' + JSON.stringify(operation), 6,
'wiki_API_cache');
if (typeof _this !== 'object') {
// _this: 傳遞於各 operator 間的 ((this))。
_this = Object.create(null);
}
var file_name = operation.file_name,
/** 前一次之回傳 data。每次產出的 data。 */
last_data_got = operation.last_data_got;
if (typeof file_name === 'function') {
// @see wiki_API_cache.title_only
file_name = file_name.call(_this, last_data_got, operation);
}
var
/** {String}method to get data */
type = operation.type,
/** {Boolean}是否自動嘗試建立目錄。 */
try_mkdir = typeof library_namespace.fs_mkdir === 'function'
&& operation.mkdir,
//
operator = typeof operation.operator === 'function'
&& operation.operator,
//
list = operation.list;
if (!file_name) {
// 若自行設定了檔名,則慢點執行 list(),先讀讀 cache。因為 list() 可能會頗耗時間。
// 基本上,設定 this.* 應該在 operation.operator() 中,而不是在 operation.list() 中。
if (typeof list === 'function') {
// TODO: 允許非同步方法。
list = list.call(_this, last_data_got, operation);
}
if (!operation.postfix) {
if (type === 'file')
operation.postfix = '.txt';
else if (type === 'URL')
operation.postfix = '.htm';
}
// 自行設定之檔名 operation.file_name 優先度較 type/title 高。
// 需要自行創建目錄!
file_name = _this[type + '_prefix'] || type;
file_name = [ file_name
// treat file_name as directory
? /[\\\/]/.test(file_name) ? file_name : file_name + '/' : '',
//
wiki_API.is_page_data(list) ? list.title
// 若 Array.isArray(list),則 ((file_name = ''))。
: typeof list === 'string' && wiki_API.normalize_title(list, true) ];
if (file_name[1]) {
file_name = file_name[0]
// 正規化檔名。
+ file_name[1].replace(/\//g, '_');
} else {
// assert: node_fs.readFile('') 將執行 callback(error)
file_name = '';
}
}
if (file_name) {
if (!('postfix' in operation) && !('postfix' in _this)
&& /\.[a-z\d\-]+$/i.test(file_name)) {
// 若已設定 filename extension則不自動添加。
operation.postfix = '';
}
file_name = [ 'prefix' in operation ? operation.prefix
// _this.prefix: cache path prefix
: 'prefix' in _this
//
? _this.prefix : wiki_API_cache.prefix, file_name,
// auto detect filename extension
'postfix' in operation ? operation.postfix
//
: 'postfix' in _this ? _this.postfix : wiki_API_cache.postfix ];
library_namespace.debug('Pre-normalized cache file name: ['
+ file_name + ']', 5, 'wiki_API_cache');
if (false)
library_namespace.debug('file name param:'
+ [ operation.file_name, _this[type + '_prefix'], type,
JSON.stringify(list) ].join(';'), 6,
'wiki_API_cache');
// 正規化檔名。
file_name = file_name.join('').replace(/[:*?<>]/g, '_');
}
library_namespace.debug('Try to read cache file: [' + file_name + ']',
3, 'wiki_API_cache');
var
/**
* 採用 JSON<br />
* TODO: parse & stringify 機制
*
* @type {Boolean}
*/
using_JSON = 'json' in operation ? operation.json : /\.json$/i
.test(file_name),
/** {String}file encoding for fs of node.js. */
encoding = _this.encoding || wiki_API.encoding;
// list file path
_this.file_name = file_name;
// console.log('Read file: ' + file_name);
node_fs.readFile(file_name, encoding, function(error, data) {
/**
* 結束作業。
*/
function finish_work(data) {
library_namespace.debug('finish work', 3,
'wiki_API_cache.finish_work');
last_data_got = data;
if (operator)
operator.call(_this, data, operation);
library_namespace.debug('loading callback', 3,
'wiki_API_cache.finish_work');
if (typeof callback === 'function')
callback.call(_this, data);
}
if (!operation.reget && !error && (data ||
// 當資料 Invalid例如採用 JSON 卻獲得空資料時;則視為 error不接受此資料。
('accept_empty_data' in _this
//
? _this.accept_empty_data : !using_JSON))) {
// gettext_config:{"id":"using-cached-data"}
library_namespace.debug('Using cached data.', 3,
'wiki_API_cache');
library_namespace.debug('Cached data: ['
+ (data && data.slice(0, 200)) + ']...', 5,
'wiki_API_cache');
if (using_JSON && data) {
try {
data = JSON.parse(data);
} catch (e) {
library_namespace.error(
// error. e.g., "undefined"
'wiki_API_cache: Cannot parse as JSON: ' + data);
// 注意: 若中途 abort此時可能需要手動刪除大小為 0 的 cache file
data = undefined;
}
}
finish_work(data);
return;
}
library_namespace.debug(
operation.reget ? 'Dispose cache. Reget again.'
// ↑ operation.reget: 放棄 cache重新取得資料。
: 'No valid cached data. Try to get data...', 3,
'wiki_API_cache');
/**
* 寫入 cache 至檔案系統。
*/
function write_cache(data) {
if (operation.cache === false) {
// 當設定 operation.cache: false 時,不寫入 cache。
library_namespace.debug(
'設定 operation.cache === false不寫入 cache。', 3,
'wiki_API_cache.write_cache');
} else if (/[^\\\/]$/.test(file_name)) {
library_namespace.info('wiki_API_cache: '
+ 'Write cache data to [' + file_name + '].'
+ (using_JSON ? ' (using JSON)' : ''));
library_namespace.debug('Cache data: '
+ (data && JSON.stringify(data).slice(0, 190))
+ '...', 3, 'wiki_API_cache.write_cache');
var write = function() {
// 為了預防需要建立目錄,影響到後面的作業,
// 因此採用 fs.writeFileSync() 而非 fs.writeFile()。
node_fs.writeFileSync(file_name, using_JSON ? JSON
.stringify(data) : data, encoding);
};
try {
write();
} catch (error) {
// assert: 此 error.code 表示上層目錄不存在。
var matched = error.code === 'ENOENT'
// 未設定 operation.mkdir 的話,預設會自動嘗試建立目錄。
&& try_mkdir !== false
//
&& file_name.match(/[\\\/][^\\\/]+$/);
if (matched) {
// 僅測試一次。設定 "已嘗試過" flag。
try_mkdir = false;
// create parent directory
library_namespace.fs_mkdir(file_name.slice(0,
matched.index));
// re-write file again.
try {
write();
} catch (e) {
library_namespace.error(
//
'wiki_API_cache: Error to write cache data!');
library_namespace.error(e);
}
}
}
}
finish_work(data);
}
// node.js v0.11.16: In strict mode code, functions can only be
// declared
// at top level or immediately within another function.
/**
* 取得並處理下一項 data。
*/
function get_next_item(data) {
if (index < list.length) {
// 利用基本相同的參數以取得 cache。
_operation.list = list[index++];
var message = '處理多項列表作業: ' + type + ' ' + index + '/'
+ list.length;
if (list.length > 8) {
library_namespace.info('wiki_API_cache.get_next_item: '
+ message);
} else {
library_namespace.debug(message, 1,
'wiki_API_cache.get_next_item');
}
wiki_API_cache(_operation, get_next_item, _this);
} else {
// last 收尾
// All got. retrieve data.
if (_operation.data_list)
data = _operation.data_list;
if (typeof operation.retrieve === 'function')
data = operation.retrieve.call(_this, data);
write_cache(data);
}
}
if (typeof list === 'function' && type !== 'callback') {
library_namespace.debug('Call .list()', 3, 'wiki_API_cache');
list = list.call(_this, last_data_got, operation);
// 對於 .list() 為 asynchronous 函數的處理。
if (list === wiki_API_cache.abort) {
library_namespace.debug('It seems the .list()'
+ ' is an asynchronous function.' + ' I will exit'
+ ' and wait for the .list() finished.', 3,
'wiki_API_cache');
return;
}
}
if (list === wiki_API_cache.abort) {
library_namespace
.debug('Abort operation.', 1, 'wiki_API_cache');
finish_work();
return;
}
if (Array.isArray(list)) {
if (!type) {
library_namespace.debug('採用 list (length ' + list.length
+ ') 作為 data。', 1, 'wiki_API_cache');
write_cache(list);
return;
}
if (list.length > 1e6) {
library_namespace.warn(
//
'wiki_API_cache: 警告: list 過長/超過限度 (length ' + list.length
+ '),將過於耗時而不實際!');
}
/**
* 處理多項列表作業。
*/
var index = 0, _operation = Object.clone(operation);
// 個別頁面不設定 .file_name, .end。
delete _operation.end;
if (_operation.each_file_name) {
_operation.file_name = _operation.each_file_name;
delete _operation.each_file_name;
} else {
delete _operation.file_name;
}
if (typeof _operation.each === 'function') {
// 每一項 list 之項目執行一次 .each()。
_operation.operator = _operation.each;
delete _operation.each;
} else {
if (typeof _operation.each_retrieve === 'function')
_operation.each_retrieve = _operation.each_retrieve
.bind(_this);
else
delete _operation.each_retrieve;
/**
* 預設處理列表的函數。
*/
_operation.operator = function(data) {
if ('each_retrieve' in operation)
// 資料事後處理程序 (post-processor):
// 將以 .each_retrieve() 的回傳作為要處理的資料。
data = operation.each_retrieve.call(_this, data);
if (_operation.data_list) {
if (Array.isArray(data))
Array.prototype.push.apply(
_operation.data_list, data);
else if (data)
_operation.data_list.push(data);
} else {
if (Array.isArray(data))
_operation.data_list = data;
else if (data)
_operation.data_list = [ data ];
}
};
}
library_namespace.debug('處理多項列表作業, using operation: '
+ JSON.stringify(_operation), 5, 'wiki_API_cache');
get_next_item();
return;
}
// ------------------------------------------------
/**
* 以下為處理單一項作業。
*/
var to_get_data, list_type;
if (// type in get_list.type
wiki_API.list.type_list.includes(type)) {
list_type = type;
type = 'list';
}
switch (type) {
case 'callback':
if (typeof list !== 'function') {
library_namespace
.warn('wiki_API_cache: list is not function!');
callback.call(_this, last_data_got);
break;
}
// 手動取得資料。使用 list=function(callback){callback(list);}
to_get_data = function(list, callback) {
library_namespace.log('wiki_API_cache: '
+ 'manually get data and then callback(list).');
if (typeof list === 'function') {
// assert: (typeof list === 'function') 必須自己回 call
list.call(_this, callback, last_data_got, operation);
}
};
break;
case 'file':
// 一般不應用到。
// get file 內容。
to_get_data = function(file_path, callback) {
library_namespace.log('wiki_API_cache: Get file ['
+ file_path + '].');
node_fs.readFile(file_path, operation.encoding, function(
error, data) {
if (error)
library_namespace.error(
//
'wiki_API_cache: Error get file [' + file_path
+ ']: ' + error);
callback.call(_this, data);
});
};
break;
case 'URL':
// get URL 頁面內容。
to_get_data = function(URL, callback) {
library_namespace.log('wiki_API_cache: Get URL of [' + URL
+ '].');
library_namespace.get_URL(URL, callback);
};
break;
case 'wdq':
to_get_data = function(query, callback) {
if (_this[KEY_SESSION]) {
if (!_this[KEY_SESSION].data_session) {
_this[KEY_SESSION].set_data();
_this[KEY_SESSION].run(function() {
// retry again
to_get_data(query, callback);
});
return;
}
operation[KEY_SESSION]
//
= _this[KEY_SESSION].data_session;
}
library_namespace.log('wiki_API_cache: Wikidata Query ['
+ query + '].');
// wikidata_query(query, callback, options)
wiki_API.wdq(query, callback, operation);
};
break;
case 'page':
// get page contents 頁面內容。
// title=(operation.title_prefix||_this.title_prefix)+operation.list
to_get_data = function(title, callback) {
library_namespace.log('wiki_API_cache: Get content of '
+ wiki_API.title_link_of(title));
// 防止汙染。
var _options = library_namespace.new_options(_this,
operation);
// 包含 .list 時wiki_API.page() 不會自動添加 .prop。
delete _options.list;
wiki_API.page(title, function(page_data) {
callback(page_data);
}, _options);
};
break;
case 'redirects_here':
// 取得所有重定向到(title重定向標的)之頁面列表,(title重定向標的)將會排在[0]。
// 注意: 無法避免雙重重定向問題!
to_get_data = function(title, callback) {
// wiki_API.redirects_here(title, callback, options)
wiki_API.redirects_here(title, function(root_page_data,
redirect_list) {
if (!operation.keep_redirects && redirect_list
&& redirect_list[0]) {
if (false) {
console.assert(redirect_list[0].redirects
//
.join() === redirect_list.slice(1).join());
}
// cache 中不需要此累贅之資料。
delete redirect_list[0].redirects;
delete redirect_list[0].redirect_list;
}
callback(redirect_list);
}, Object.assign({
// Making .redirect_list[0] the redirect target.
include_root : true
}, _this, operation));
};
break;
case 'list':
to_get_data = function(title, callback) {
var options = Object.assign({
type : list_type
}, _this, operation);
wiki_API.list(title, function(pages) {
if (!options.for_each_page || options.get_list) {
library_namespace.log(list_type
// allpages 不具有 title。
+ (title ? ' '
//
+ wiki_API.title_link_of(title) : '') + ': '
+ pages.length + ' page(s).');
}
pages.query_title = title;
// page list, title page_data
callback(pages);
}, options);
};
break;
default:
if (typeof type === 'function')
to_get_data = type.bind(Object.assign(Object.create(null),
_this, operation));
else if (type)
throw new Error('wiki_API_cache: Bad type: ' + type);
else {
library_namespace.debug('直接採用 list 作為 data。', 1,
'wiki_API_cache');
write_cache(list);
return;
}
}
// 回復 recover type
// if (list_type) type = list_type;
var title = list;
if (typeof title === 'string') {
// 可以用 operation.title_prefix 覆蓋 _this.title_prefix
if ('title_prefix' in operation) {
if (operation.title_prefix)
title = operation.title_prefix + title;
} else if (_this.title_prefix)
title = _this.title_prefix + title;
}
library_namespace.debug('處理單一項作業: ' + wiki_API.title_link_of(title)
+ '。', 3, 'wiki_API_cache');
to_get_data(title, write_cache);
});
}
/** {String}預設 file encoding for fs of node.js。 */
wiki_API.encoding = 'utf8';
/** {String}檔名預設前綴。 */
wiki_API_cache.prefix = '';
/** {String}檔名預設後綴。 */
wiki_API_cache.postfix = '.json';
/**
* 若 operation.list() return wiki_API_cache.abort<br />
* 則將直接中斷離開 operation不執行 callback。<br />
* 此時須由 operation.list() 自行處理 callback。
*/
wiki_API_cache.abort = typeof Symbol === 'function' ? Symbol('ABORT_CACHE')
//
: {
cache : 'abort'
};
/**
* 只取檔名,僅用在 operation.each_file_name。<br />
* <code>{
* each_file_name : CeL.wiki.cache.title_only,
* }</code>
*
* @type {Function}
*/
wiki_API_cache.title_only = function(last_data_got, operation) {
var list = operation.list;
if (typeof list === 'function') {
operation.list = list = list.call(this, last_data_got, operation);
}
return operation.type + '/' + remove_page_title_namespace(list);
};
// ------------------------------------------------------------------------
// export 導出.
// wiki_API.cache = wiki_API_cache;
return wiki_API_cache;
}

6212
app/node_modules/cejs/application/net/wiki/data.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

1646
app/node_modules/cejs/application/net/wiki/edit.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,483 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): 特色內容特設功能。
*
* 注意: 本程式庫必須應各wiki特色內容改動而改寫。
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* @example <code>
CeL.run('application.net.wiki.featured_content');
wiki.get_featured_content('FFA', function(FC_data_hash) {});
wiki.get_featured_content('GA', function(FC_data_hash) {});
wiki.get_featured_content('FA', function(FC_data_hash) {});
wiki.get_featured_content('FL', function(FC_data_hash) {});
</code>
*
* @since 2020/1/22 9:18:43
*/
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.featured_content',
require : 'data.native.' + '|application.net.wiki.'
// load MediaWiki module basic functions
+ '|application.net.wiki.namespace.'
// for to_exit
+ '|application.net.wiki.parser.'
//
+ '|application.net.wiki.page.|application.net.wiki.list.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki, KEY_SESSION = wiki_API.KEY_SESSION;
// @inner
// var is_api_and_title = wiki_API.is_api_and_title,
// normalize_title_parameter = wiki_API.normalize_title_parameter;
var to_exit = wiki_API.parser.parser_prototype.each.exit;
// --------------------------------------------------------------------------------------------
function featured_content() {
}
function get_parsed(page_data) {
if (!page_data)
return;
var parsed = typeof page_data.each === 'function'
// `page_data` is parsed data
? page_data : wiki_API.parser(page_data);
return parsed;
}
// ------------------------------------------------------------------------
/** 特色內容為列表 */
var KEY_IS_LIST = 'is_list';
/** 為已撤銷的特色內容 */
var KEY_ISFFC = 'is_former';
/** 特色內容類別 */
var KEY_CATEGORY = 'category';
/** 指示用。會在 parse_each_zhwiki_FC_item_list_page() 之後就刪除。 */
var KEY_LIST_PAGE = 'list page';
function remove_KEY_LIST_PAGE(FC_data_hash) {
for ( var title in FC_data_hash) {
delete FC_data_hash[title][KEY_LIST_PAGE];
}
}
var featured_content_configurations = {
zhwiki : {
// @see [[Category:特色内容]]
list_source : {
FA : '典范条目‎',
FL : '特色列表‎',
FP : '特色图片‎',
GA : '優良條目‎',
},
get_FC : /* get_zhwiki_FC_via_list_page */get_FC_via_category
},
jawiki : {
// @see [[ja:Category:記事の選考]]
list_source : {
FA : 'ウィキペディア 秀逸な記事',
FL : 'ウィキペディア 秀逸な一覧',
FP : 'ウィキペディア 秀逸な画像',
GA : 'ウィキペディア 良質な記事'
},
get_FC : get_FC_via_category
},
enwiki : {
// @see [[en:Category:Featured content]]
list_source : {
FFA : {
page : 'Wikipedia:Former featured articles',
handler : parse_enwiki_FFA
},
DGA : 'Delisted good articles',
FA : 'Featured articles',
FL : 'Featured lists',
FP : 'Featured pictures',
FT : 'Featured topics',
GA : 'Good articles'
},
get_FC : get_FC_via_category
}
};
function get_site_configurations(session) {
// e.g., 'zhwiki'
var site_name = wiki_API.site_name(session);
var FC_configurations = featured_content_configurations[site_name];
return FC_configurations;
}
// @see 20190101.featured_content_maintainer.js
// 注意: 這邊尚未處理 redirects 的問題!!
function parse_each_zhwiki_FC_item_list_page(page_data, redirects_to_hash,
sub_FC_list_pages) {
var using_GA = options.type === 'GA';
/** {String}將顯示的類型名稱。 */
var TYPE_NAME = using_GA ? '優良條目' : '特色內容';
/** {Array}錯誤記錄 */
var error_logs = [];
var FC_data_hash = this.FC_data_hash
// FC_data_hash[redirected FC_title] = { FC_data }
|| (this.FC_data_hash = Object.create(null));
/**
* {String}page title = page_data.title
*/
var title = wiki_API.title_of(page_data);
/**
* {String}page content, maybe undefined. 條目/頁面內容 =
* wiki_API.revision_content(revision)
*/
var content = wiki_API.content_of(page_data);
//
var matched;
/** 特色內容為列表 */
var is_list = /list|列表/.test(title)
// e.g., 'Wikipedia:FL'
|| /:[DF]?[FG]L/.test(page_data.original_title || title),
// 本頁面為已撤消的條目列表。注意: 這包含了被撤銷後再次被評為典範的條目。
is_FFC = [ page_data.original_title, title ].join('|');
// 對於進階的條目,採用不同的 is_FFC 表示法。
is_FFC = using_GA && /:FF?A/.test(is_FFC) && 'UP'
|| /:[DF][FG][AL]|已撤消的|已撤销的/.test(is_FFC);
if (is_FFC) {
// 去掉被撤銷後再次被評為典範的條目/被撤銷後再次被評為特色的列表/被撤銷後再次被評選的條目
content = content.replace(/\n== *(?:被撤銷後|被撤销后)[\s\S]+$/, '');
}
// 自動偵測要使用的模式。
function test_pattern(pattern, min) {
var count = 0, matched;
while (matched = pattern.exec(content)) {
if (matched[1] && count++ > (min || 20)) {
return pattern;
}
}
}
var catalog,
// matched: [ all, link title, display, catalog ]
PATTERN_Featured_content = test_pattern(
// @see [[Template:FA number]] 被標記為粗體的條目已經在作為典範條目時在首頁展示過
// 典範條目, 已撤銷的典範條目, 已撤销的特色列表: '''[[title]]'''
// @see PATTERN_category
/'''\[\[([^{}\[\]\|<>\t\n#<23>]+)(?:\|([^\[\]\|<7C>]*))?\]\]'''|\n==([^=].*?)==\n/g)
// 特色列表: [[:title]]
|| test_pattern(/\[\[:([^{}\[\]\|<>\t\n#<23>]+)(?:\|([^\[\]\|<7C>]*))?\]\]|\n==([^=].*?)==\n/g)
// 優良條目轉換到子頁面模式: 警告:本頁中的所有嵌入頁面都會被機器人當作優良條目的分類列表。請勿嵌入非優良條目的分類列表。
|| test_pattern(/{{(Wikipedia:[^{}\|]+)}}/g, 10)
// 優良條目子分類列表, 已撤消的優良條目: all links NOT starting with ':'
|| /\[\[([^{}\[\]\|<>\t\n#<23>:][^{}\[\]\|<>\t\n#<23>]*)(?:\|([^\[\]\|<7C>]*))?\]\]|\n===([^=].*?)===\n/g;
library_namespace.log(wiki_API.title_link_of(title)
+ ': '
+ (is_FFC ? 'is former'
+ (is_FFC === true ? '' : ' (' + is_FFC + ')')
: 'NOT former') + ', '
+ (is_list ? 'is list' : 'is article') + ', using pattern '
+ PATTERN_Featured_content);
// reset pattern
PATTERN_Featured_content.lastIndex = 0;
// 分類/類別。
if (matched = title.match(/\/(?:分類|分类)\/([^\/]+)/)) {
catalog = matched[1];
}
if (false) {
library_namespace.log(content);
console.log([ page_data.original_title || title, is_FFC, is_list,
PATTERN_Featured_content ]);
}
while (matched = PATTERN_Featured_content.exec(content)) {
// 還沒繁簡轉換過的標題。
var original_FC_title = wiki_API.normalize_title(matched[1]);
if (matched.length === 2) {
sub_FC_list_pages.push(original_FC_title);
continue;
}
// assert: matched.length === 4
if (matched[3]) {
// 分類/類別。
catalog = matched[3].replace(/<!--[\s\S]*?-->/g, '').trim()
.replace(/\s*\d+$/, '');
continue;
}
// 去除並非文章,而是工作連結的情況。 e.g., [[File:文件名]], [[Category:维基百科特色内容|*]]
if (this.namespace(original_FC_title, 'is_page_title') !== 0) {
continue;
}
// 轉換成經過繁簡轉換過的最終標題。
var FC_title = redirects_to_hash
&& redirects_to_hash[original_FC_title]
|| original_FC_title;
if (FC_title in FC_data_hash) {
// 基本檢測與提醒。
if (FC_data_hash[FC_title][KEY_ISFFC] === is_FFC) {
library_namespace.warn(
//
'parse_each_zhwiki_FC_item_list_page: Duplicate '
+ TYPE_NAME + ' title: ' + FC_title + '; '
+ JSON.stringify(FC_data_hash[FC_title]) + '; '
+ matched[0]);
error_logs.push(wiki_API.title_link_of(title)
+ '有重複條目: '
+ wiki_API.title_link_of(original_FC_title)
+ (original_FC_title === FC_title ? '' : ', '
+ wiki_API.title_link_of(FC_title)));
} else if (!!FC_data_hash[FC_title][KEY_ISFFC] !== !!is_FFC
&& (FC_data_hash[FC_title][KEY_ISFFC] !== 'UP' || is_FFC !== false)) {
error_logs
.push(wiki_API.title_link_of(FC_title)
+ ' 被同時列在了現存及已撤銷的'
+ TYPE_NAME
+ '清單中: '
+ wiki_API.title_link_of(original_FC_title)
+ '@'
+ wiki_API.title_link_of(title)
+ ', '
+ wiki_API
.title_link_of(FC_data_hash[FC_title][KEY_LIST_PAGE][1])
+ '@'
+ wiki_API
.title_link_of(FC_data_hash[FC_title][KEY_LIST_PAGE][0]));
library_namespace.error(wiki_API.title_link_of(FC_title)
+ ' 被同時列在了現存及已撤銷的' + TYPE_NAME + '清單中: ' + is_FFC
+ '; ' + JSON.stringify(FC_data_hash[FC_title]));
}
}
var FC_data = FC_data_hash[FC_title] = Object.create(null);
FC_data[KEY_IS_LIST] = is_list;
FC_data[KEY_ISFFC] = is_FFC;
if (catalog)
FC_data[KEY_CATEGORY] = catalog;
FC_data[KEY_LIST_PAGE] = [ title, original_FC_title ];
}
return error_logs;
}
function get_zhwiki_FC_via_list_page(options, callback) {
var session = this;
var using_GA = options.type === 'GA';
var FC_list_pages = (using_GA ? 'WP:GA' : 'WP:FA|WP:FL').split('|');
var Former_FC_list_pages = (using_GA ? 'WP:DGA|WP:FA|WP:FFA'
: 'WP:FFA|WP:FFL').split('|');
var page_options = {
redirects : 1,
multi : true
};
this.page(FC_list_pages.concat(Former_FC_list_pages), function(
page_data_list) {
var sub_FC_list_pages = [];
page_data_list.forEach(function(page_data) {
parse_each_zhwiki_FC_item_list_page.call(session, page_data,
options.redirects_to_hash, sub_FC_list_pages);
});
if (sub_FC_list_pages.length === 0) {
remove_KEY_LIST_PAGE(session.FC_data_hash);
callback && callback(session.FC_data_hash);
return;
}
session.page(sub_FC_list_pages, function(page_data_list) {
page_data_list.forEach(function(page_data) {
parse_each_zhwiki_FC_item_list_page.call(session,
page_data, options.redirects_to_hash);
});
remove_KEY_LIST_PAGE(session.FC_data_hash);
callback && callback(session.FC_data_hash);
}, page_options);
}, page_options);
}
// ------------------------------------------------------------------------
function parse_enwiki_FFA(page_data, type_name) {
/**
* {String}page content, maybe undefined. 條目/頁面內容 =
* wiki_API.revision_content(revision)
*/
var content = wiki_API.content_of(page_data);
content = content.replace(/^[\s\S]+?\n(==.+?==)/, '$1')
// remove == Former featured articles that have been re-promoted ==
.replace(/==\s*Former featured articles.+?==[\s\S]*$/, '');
var FC_data_hash = this.FC_data_hash;
var PATTERN_Featured_content = /\[\[(.+?)\]\]/g, matched;
while (matched = PATTERN_Featured_content.exec(content)) {
var FC_title = matched[1];
var FC_data = FC_data_hash[FC_title];
if (FC_data) {
if (!FC_data.types.includes(type_name)) {
// 把重要的放在前面。
FC_data.types.unshift(type_name);
}
// Do not overwrite
continue;
}
FC_data = FC_data_hash[FC_title] = {
type : type_name,
types : [ type_name ]
};
FC_data[KEY_ISFFC] = true;
// FC_data[KEY_IS_LIST] = is_list;
}
}
// ------------------------------------------------------------------------
function normalize_type_name(type) {
return type;
}
function get_FC_via_category(options, callback) {
var FC_configurations = get_site_configurations(this);
var type_name = normalize_type_name(options.type);
var list_source = FC_configurations.list_source[type_name];
// console.trace([ FC_configurations, type_name, list_source ]);
if (!list_source) {
throw new Error('Unknown type: ' + options.type);
}
// ----------------------------
var FC_data_hash = this.FC_data_hash
// FC_data_hash[redirected FC_title] = { FC_data }
|| (this.FC_data_hash = Object.create(null));
// ----------------------------
var session = this;
if (list_source.page) {
this.page(list_source.page, function(page_data) {
list_source.handler.call(session, page_data, type_name);
callback && callback(FC_data_hash);
});
return;
}
// ----------------------------
var category_title = list_source;
/** 特色內容為列表 */
var is_list = /list|列表/.test(category_title);
wiki_API.list(category_title, function(list/* , target, options */) {
list.forEach(function(page_data) {
var FC_title = page_data.title;
var FC_data = FC_data_hash[FC_title];
if (!FC_data) {
FC_data = FC_data_hash[FC_title] = {
type : type_name,
types : [ type_name ]
};
} else if (FC_data.type !== type_name) {
if (FC_data.type !== 'FFA' || type_name === 'FA') {
if (options.on_conflict) {
options.on_conflict(FC_title, {
from : FC_data.type,
to : type_name,
category : category_title
});
} else {
library_namespace.warn('get_FC_via_category: '
+ FC_title + ': ' + FC_data.type + '→'
+ type_name);
}
}
if (!FC_data.types.includes(type_name)) {
// 把重要的放在前面。
FC_data.types.unshift(type_name);
}
FC_data.type = type_name;
}
FC_data[KEY_IS_LIST] = is_list;
// FC_data[KEY_ISFFC] = false;
// if (catalog) FC_data[KEY_CATEGORY] = catalog;
});
callback && callback(FC_data_hash);
}, {
// [KEY_SESSION]
session : this,
// namespace: '0|1',
type : 'categorymembers'
});
}
// --------------------------------------------------------------------------------------------
// export 導出.
// Object.assign(featured_content, {});
// ------------------------------------------------------------------------
// wrapper for local function
wiki_API.prototype.get_featured_content_configurations = function get_featured_content_configurations() {
return get_site_configurations(this);
};
// callback(wiki.FC_data_hash);
// e.g.,
// wiki.FC_data_hash[title]={type:'GA',types:['GA','FFA'],is_former:true,is_list:false}
wiki_API.prototype.get_featured_content = function get_featured_content(
options, callback) {
var FC_configurations = this.get_featured_content_configurations();
var get_FC_function = FC_configurations && FC_configurations.get_FC;
if (!get_FC_function) {
library_namespace.error('get_featured_content: '
+ 'Did not configured how to get featured content! '
+ wiki_API.site_name(this));
return;
}
if (typeof options === 'string') {
options = {
type : options
};
} else {
options = library_namespace.setup_options(options);
}
get_FC_function.call(this, options, callback);
};
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
// return featured_content;
}

2859
app/node_modules/cejs/application/net/wiki/list.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

4003
app/node_modules/cejs/application/net/wiki/namespace.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

4723
app/node_modules/cejs/application/net/wiki/page.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

320
app/node_modules/cejs/application/net/wiki/page/Page.js generated vendored Normal file
View File

@@ -0,0 +1,320 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): Page
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
</code>
*
* @since
*
* @see https://mwn.toolforge.org/docs/interfaces/_page_.mwnpage.html
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.page.Page',
require : 'data.code.compatibility.'
//
+ '|application.net.wiki.page.'
//
+ '|application.net.wiki.list.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki, KEY_SESSION = wiki_API.KEY_SESSION;
// @inner
// var get_namespace = wiki_API.namespace;
// ------------------------------------------------------------------------
if (false) {
// call new_Page()
page = wiki_session.Page(page_title);
// {Number}p.ns
// {String}p.title
// await page.backlinks({get_list:true}) will get {Array}list.
// page.backlinks() is asyncIterator
//
// https://www.codementor.io/@tiagolopesferreira/asynchronous-iterators-in-javascript-jl1yg8la1
// https://stackoverflow.com/questions/55531247/using-javascripts-symbol-asynciterator-with-for-await-of-loop
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Iteration_protocols
// for await (const page_data of page.backlinks()) {
// console.log(page_data); }
// TODO:
// typeof await(page.content() || page.read()) === 'string';
typeof page.wikitext === 'string';
// page.fetch() is asyncIterator 泛用方法 相當於 wiki.query()
// page.revisions() is asyncIterator
// typeof await(page.is_biography()) === 'boolean';
}
function Page(page_title, options, session) {
this[KEY_SESSION] = session;
if (wiki_API.is_page_data(page_title)) {
Object.assign(this, page_title);
return;
}
// page_data 之 structure 按照 wiki API 本身之 return
// page_data = {pageid,ns,title,revisions:[{revid,timestamp,'*'}]}
Object.assign(this, {
// pageid : 0,
ns : session.namespace(page_title) || 0,
title : session.normalize_title(page_title)
});
}
function set_options_session(options) {
var session = this[KEY_SESSION];
options = wiki_API.add_session_to_options(session, options);
return options;
}
// ------------------------------------------------------------------------
function Page__content(options) {
options = set_options_session.call(this, options);
if (this.revisions) {
return wiki_API.content_of(this, options);
}
var promise = new Promise(function executor(resolve, reject) {
wiki_API.page(wiki_API.is_page_data(this) ? this : this.title,
//
function(page_data, error) {
if (error) {
reject(error);
return;
}
if (!page_data.revisions) {
reject(new Error('No .revisions get!'));
return;
}
Object.assign(this, page_data);
// console.trace(this);
return resolve(wiki_API.content_of(this, options));
}.bind(this), options);
}.bind(this));
return promise;
}
// ------------------------------------------------------------------------
function Page__check_stop(options) {
// Copy from wiki_API.prototype.next.edit
var promise = new Promise(function executor(resolve, reject) {
var session = this[KEY_SESSION];
options.token = session.token;
wiki_API.check_stop(function(stopped) {
session.stopped = stopped;
resolve(stopped);
}, options);
}.bind(this));
return promise;
}
function Page__edit(content, options) {
options = set_options_session.call(this, options);
var session = this[KEY_SESSION];
// Copy from wiki_API.prototype.next.edit
var promise = new Promise(function executor(resolve, reject) {
if (session.stopped && !options.skip_stopped) {
library_namespace.warn('Page__edit: 已停止作業,放棄編輯'
+ wiki_API.title_link_of(this) + '');
reject(new Error('放棄編輯'));
return;
}
if (this.is_Flow) {
reject(new Error(new Error('NYI: flow page')));
}
if (options.skip_nochange
// 採用 skip_nochange 可以跳過實際 edit 的動作。
&& content === wiki_API.content_of(this)) {
library_namespace.debug('Skip '
//
+ wiki_API.title_link_of(this)
// 'nochange', no change
+ ': The same content.', 1, 'Page__edit');
resolve('The same content');
return;
}
// console.trace([ this, wiki_API.is_page_data(this), session.token
// ]);
wiki_API.edit([ session.API_URL,
//
wiki_API.is_page_data(this) ? this : this.title ], content,
//
session.token, options, function wiki_API_Page_edit_callback(title,
error, result) {
if (error) {
reject(error);
return;
}
resolve(result);
});
}.bind(this));
if (!('stopped' in session)) {
promise = Page__check_stop.call(this, options).then(promise);
}
if (typeof content === 'function')
return this.content().then(promise);
return promise;
}
// ------------------------------------------------------------------------
function Page__list(options) {
options = set_options_session.call(this, options);
// options.type, options[KEY_SESSION] are setted in Page__list_async()
var promise = new Promise(function executor(resolve, reject) {
wiki_API.list(wiki_API.is_page_data(this) ? this : this.title,
//
function(pages, target, options) {
if (pages.error)
reject(pages.error);
else
resolve(pages);
}, options);
}.bind(this));
return promise;
}
var Symbol_asyncIterator = typeof Symbol === 'function'
&& Symbol.asyncIterator;
var done_object = {
// value : generator.page_count,
done : true
};
function Page__list_async(method, options) {
options = set_options_session.call(this, options);
options.type = method;
if (!Symbol_asyncIterator || options && options.get_list) {
return Page__list.call(this, options);
}
// --------------------------------------
var list_generator = Object.create(null);
list_generator[Symbol_asyncIterator] = (function() {
function get_next_object() {
return {
value : generator.queue.shift(),
done : false
};
}
var generator = {
queue : [],
next : function() {
if (generator.resolve) {
throw new Error(
'Call resolve() before latest promise resolved');
}
if (generator.queue.length > 0) {
// 執行順序3: 中間最多的是這個程序一直反覆 loop
return Promise.resolve(get_next_object());
}
// assert: generator.queue.length === 0
if (generator.done) {
// 執行順序4: 最後一次 iterate
return Promise.resolve(done_object);
}
// 執行順序1
return new Promise(function(resolve, reject) {
generator.resolve = resolve;
});
}
};
options.for_each_page = function(item) {
generator.queue.push(item);
var resolve = generator.resolve;
if (resolve) {
delete generator.resolve;
// 執行順序2
resolve(get_next_object());
}
};
wiki_API.list(wiki_API.is_page_data(this) ? this : this.title,
//
function(list) {
// generator.page_count = list.length;
generator.done = true;
var resolve = generator.resolve;
if (resolve) {
// 基本上不會執行到這邊 @ node.js
delete generator.resolve;
resolve(done_object);
}
}, options);
return generator;
}).bind(this);
return list_generator;
}
// ------------------------------------------------------------------------
// export 導出.
Object.assign(wiki_API.prototype, {
new_Page : function new_Page(page_title, options) {
return new Page(page_title, options,/* session */this);
},
Page : Page
});
wiki_API.list.type_list.forEach(function(method) {
// if (!method.includes('all'))
Page.prototype[method] = function Page__list_frontend(options) {
return Page__list_async.call(this, method, options);
};
});
Object.assign(Page.prototype, {
content : Page__content,
edit : Page__edit
});
return Page;
}

2078
app/node_modules/cejs/application/net/wiki/parser.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

2145
app/node_modules/cejs/application/net/wiki/parser/misc.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

1109
app/node_modules/cejs/application/net/wiki/query.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

4305
app/node_modules/cejs/application/net/wiki/task.js generated vendored Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,90 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): 常用模板特設功能。本工具檔放置的是指定 wiki
* 計畫特有的模板。
*
* 注意: 本程式庫必須應各 wiki project 模板內容改動而改寫。
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
</code>
*
* @since 2021/1/24 16:6:50
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.template_functions.enwiki',
require : 'data.native.'
// Should also load essential MediaWiki modules
+ '|application.net.wiki.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki;
// @inner
// var is_api_and_title = wiki_API.is_api_and_title,
// normalize_title_parameter = wiki_API.normalize_title_parameter;
var to_exit = wiki_API.parser.parser_prototype.each.exit;
// e.g., 'zhwiki'
var module_site_name = this.id.match(/[^.]+$/)[0];
function empty_string(/* options */) {
// var template_token = this;
return '';
}
// --------------------------------------------------------------------------------------------
// template_token.expand() 可將模板轉換成一般 wiki 語法。
// https://www.mediawiki.org/w/api.php?action=help&modules=expandtemplates
// 用於 function preprocess_section_link_token()。
// --------------------------------------------------------------------------------------------
// Not completed! Only for get_all_anchors().
function expand_template_Football_box(options) {
var parameters = this.parameters;
// [[Module:Football box]]
return '<div id="' + (parameters.id || '') + '">'
// TODO: The content is skipped.
+ '</div>';
}
function parse_template_Football_box(template_token, index, parent, options) {
template_token.expand = expand_template_Football_box;
}
// --------------------------------------------------------------------------------------------
// export 導出.
wiki_API.template_functions.functions_of_site[module_site_name] = {
// 一些會添加 anchors 的特殊模板。
'Football box' : parse_template_Football_box
};
// --------------------------------------------------------------------------------------------
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

View File

@@ -0,0 +1,595 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科):
* 常用模板特設功能。本工具檔放置的是幾乎所有wiki計畫通用的模板或者少數wiki計畫特有、且大量使用的著名模板。
*
* 注意: 本程式庫必須應各 wiki project 模板內容改動而改寫。
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* <code>
TODO:
[[w:en:Wikipedia:Database reports/Templates transcluded on the most pages]]
[[w:en:Wikipedia:High-risk templates]]
</code>
*
* @see [[Special:MostTranscludedPages]], [[Template:High-use]]
*
* @since 2021/1/24 16:6:50
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.template_functions.general_functions',
require : 'data.native.'
// Should also load essential MediaWiki modules
+ '|application.net.wiki.template_functions.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki;
// @inner
// var is_api_and_title = wiki_API.is_api_and_title,
// normalize_title_parameter = wiki_API.normalize_title_parameter;
var to_exit = wiki_API.parser.parser_prototype.each.exit;
function empty_string(/* options */) {
// var template_token = this;
return '';
}
function expand_template_get_parameter_1(options) {
var parameters = this.parameters;
return parameters[1] ? parameters[1].toString() : '';
}
function trim_param(param) {
return param && param.toString().trim();
}
// --------------------------------------------------------------------------------------------
// template_token.expand() 可將模板轉換成一般 wiki 語法。
// https://www.mediawiki.org/w/api.php?action=help&modules=expandtemplates
// 用於 function preprocess_section_link_token()。
// --------------------------------------------------------------------------------------------
function expand_template_Void(options) {
return '';
}
function parse_template_Void(template_token, index, parent, options) {
template_token.expand = expand_template_Void;
}
// --------------------------------------------------------------------------------------------
// Not completed! Only for get_all_anchors()
// @ zh.moegirl [[FLOWERS(Innocent Grey)]]
function parse_template_Center(template_token) {
template_token.expand = expand_template_get_parameter_1;
}
// --------------------------------------------------------------------------------------------
// Not completed! Only for get_all_anchors()
// @ zh.moegirl [[ARGONAVIS from BanG Dream! 翻唱曲列表]]
function parse_template_Font(template_token) {
template_token.expand = expand_template_get_parameter_1;
}
// --------------------------------------------------------------------------------------------
// {{color|英文顏色名稱或是RGB 16進制編碼|文字}}
function expand_template_Color(options) {
var parameters = this.parameters;
return '<span style="color:' + (parameters[1] || '') + '">'
+ (parameters[2] || parameters[1] || '') + '</span>';
}
function parse_template_Color(template_token) {
template_token.expand = expand_template_Color;
}
// --------------------------------------------------------------------------------------------
function expand_template_At(options) {
var parameters = this.parameters;
return '[[File:At_sign.svg|' + (parameters[1] || 15) + 'px|link=]]';
}
function parse_template_At(template_token) {
template_token.expand = expand_template_At;
}
// --------------------------------------------------------------------------------------------
function expand_template_User_link(options) {
var parameters = this.parameters;
return '[[User:' + (parameters[1]) + '|'
+ (parameters[2] || parameters[1]) + ']]';
}
function parse_template_User_link(template_token) {
template_token.expand = expand_template_User_link;
}
// --------------------------------------------------------------------------------------------
function expand_module_If_empty(options) {
/* const */var token = options && options.template_token_called
|| this;
/* const */var parameters = token.parameters;
// Error.stackTraceLimit = Infinity;
// console.trace([ this, parameters, options ]);
// console.trace(options && options.template_token_called);
for (/* let */var index = 0; index < token.length; index++) {
var value = parameters[index];
if (value)
return value;
}
return '';
}
function parse_module_If_empty(token) {
token.expand = expand_module_If_empty;
}
// --------------------------------------------------------------------------------------------
// Not completed! Only for get_all_anchors()
// @ zh.moegirl [[ARGONAVIS from BanG Dream! 翻唱曲列表]]
function expand_template_Colored_link(options) {
var parameters = this.parameters;
// {{Colored link|顏色|頁面名稱鏈接|顯示名稱}}]
return '[[:' + parameters[2] + '|<span style="color:' + parameters[1]
+ '">' + (parameters[3] || parameters[2]) + '</span>]]';
}
function parse_template_Colored_link(template_token) {
template_token.expand = expand_template_Colored_link;
}
// --------------------------------------------------------------------------------------------
// 一些會添加 anchors 的特殊模板。
// {{Anchor|anchor|別名1|別名2}}
function expand_template_Anchor(options) {
var parameters = this.parameters;
var wikitext = [];
for (/* let */var index = 1; index < this.length; index++) {
var anchor = parameters[index];
if (!anchor) {
continue;
}
if (typeof anchor !== 'string') {
// e.g., `{{Anchor|{{u|Emojibot}}}}` @ zhwiki
library_namespace.warn('expand_template_Anchor: 特殊 anchor: #'
+ anchor);
// console.trace(anchor);
// old jawiki {{Anchor}}
// e.g., [[終着駅シリーズ]]: {{Anchor|[[牛尾正直]]}}
// {{Anchor|A[[B|C]]}} → "AC"
anchor = wiki_API.wikitext_to_plain_text(anchor.toString());
}
// 多空格、斷行會被轉成單一 " "。
anchor = anchor.replace(/[\s\n]{2,}/g, ' ');
// class="anchor"
wikitext.push('<span id="' + anchor + '"></span>');
}
return wikitext.join('');
}
function parse_template_Anchor(template_token, index, parent, options) {
template_token.expand = expand_template_Anchor;
}
// --------------------------------------------------------------------------------------------
function expand_template_Visible_anchor(options) {
var parameters = this.parameters;
// {{Visible anchor}}(別名{{Vanc}}は似たテンプレートですが、1個目のリンク先が表示されます。
return expand_template_Anchor.call(this, options)
// + '<span class="vanchor-text">'
+ (parameters.text || parameters[1] || '')
// + '</span>'
;
}
function parse_template_Visible_anchor(template_token, index, parent,
options) {
template_token.expand = expand_template_Visible_anchor;
}
// --------------------------------------------------------------------------------------------
function expand_template_Term(options) {
var parameters = this.parameters;
var wikitext = '<dt id="'
+ wiki_API.wikitext_to_plain_text(
//
parameters.id || parameters.term || parameters[1] || '')
.replace(/"/g, '').toLowerCase()
+ '">'
+ (parameters.content || parameters[2] || parameters.term
|| parameters[1] || '') + '</dt>';
// console.log(wikitext);
return wikitext;
}
function parse_template_Term(template_token, index, parent, options) {
template_token.expand = expand_template_Term;
}
// --------------------------------------------------------------------------------------------
function expand_template_Wikicite(options) {
var parameters = this.parameters;
// class="citation wikicite"
var wikitext = '<cite id='
+ (parameters.ref || parameters.id
&& ('"Reference-' + parameters.id + '"')) + '>'
+ parameters.reference + '</cite>';
// console.log(wikitext);
return wikitext;
}
function parse_template_Wikicite(template_token, index, parent, options) {
template_token.expand = expand_template_Wikicite;
}
// --------------------------------------------------------------------------------------------
function expand_template_SfnRef(options) {
var parameters = this.parameters;
var anchor = 'CITEREF';
for (var index = 1; index <= 5 && parameters[index]; index++) {
anchor += trim_param(parameters[index]);
}
// TODO: test year
// anchor = anchor.replace(/\s+/g, ' ');
return anchor;
}
function parse_template_SfnRef(template_token, index, parent, options) {
template_token.expand = expand_template_SfnRef;
}
// --------------------------------------------------------------------------------------------
// @see local function sfn (frame) @
// https://en.wikipedia.org/wiki/Module:Footnotes
function expand_template_Sfn(options) {
var parameters = this.parameters;
var anchor = 'cite_ref-FOOTNOTE';
for (var index = 1; index <= 5 && parameters[index]; index++) {
anchor += trim_param(parameters[index]);
}
if (parameters.p)
anchor += trim_param(parameters.p);
if (parameters.pp)
anchor += trim_param(parameters.pp);
if (parameters.loc)
anchor += trim_param(parameters.loc);
anchor = anchor.replace(/\s+/g, ' ');
var wikitext = [];
var reference_index = 1;
var pointer_index = 0;
// TODO: 這個數值必須按照 reference_index 遞增。
var ref_anchor = anchor + '-' + reference_index;
wikitext.push('<ref name="' + ref_anchor + '">'
// TODO: + content
+ '</ref>',
//
'<a id="' + anchor + '_' + reference_index + '-'
// TODO: 這個數值必須按照 pointer_index 遞增。
+ pointer_index + '" href="#' + ref_anchor + '">'
// TODO: 這個數值必須按照 reference_index 遞增。
+ '[' + reference_index + ']' + '</a>');
return wikitext.join('');
}
function parse_template_Sfn(template_token, index, parent, options) {
template_token.expand = expand_template_Sfn;
}
// --------------------------------------------------------------------------------------------
// @see createEpisodeNumberCellSecondary() @ [[Module:Episode list]]
var EpisodeNumbers = [ 'EpisodeNumber', 'EpisodeNumber2', 'EpisodeNumber3' ];
function expand_template_Episode_list(options) {
// console.trace(this);
var parameters = this.parameters;
var anchor_prefix = this.anchor_prefix || '';
var wikitext = [];
for (var index = 0; index < EpisodeNumbers.length; index++) {
var anchor = trim_param(parameters[EpisodeNumbers[index]]);
// console.trace([ EpisodeNumbers[index], anchor ]);
// @see getEpisodeText() @ [[Module:Episode list]]
var matched = anchor && anchor.match(/^\w+/);
if (matched) {
anchor = matched[0];
// 極度簡化版。
wikitext.push('<th id="' + anchor_prefix + 'ep' + anchor
+ '"></th>');
}
}
// @see createProductionCodeCell() @ [[Module:Episode list]]
var anchor = trim_param(parameters.ProdCode);
if (anchor) {
wikitext.push('<td id="' + 'pc' + wiki_API.plain_text(anchor)
+ '"></td>');
}
// console.trace(wikitext);
return wikitext.join('');
}
function parse_template_Episode_list(template_token, index, parent, options) {
template_token.expand = expand_template_Episode_list;
}
function expand_template_Episode_table(options) {
}
function parse_template_Episode_table(template_token, index, parent,
options) {
// template_token.expand = expand_template_Episode_table;
var parameters = template_token.parameters;
var episodes = parameters.episodes;
var anchor_prefix = trim_param(parameters.anchor);
// console.trace(anchor_prefix);
if (anchor_prefix && episodes) {
var session = wiki_API.session_of_options(options) || wiki_API;
wiki_API.parser.parser_prototype.each.call(episodes,
//
'transclusion', function(template_token) {
if (session.is_template(template_token, [ 'Episode list',
'Episode list/sublist' ])) {
template_token.anchor_prefix = anchor_prefix;
}
}, options);
}
}
function expand_template_Episode_table__part(options) {
var parameters = this.parameters;
// console.trace(parameters);
// [[Module:Episode table]]
var id = trim_param(parameters.id);
if (!id) {
// partTypes
[ 'Act', 'Chapter', 'Part', 'Volume', 'Week' ].forEach(function(
prefix) {
var value = parameters[prefix.toLowerCase()];
if (value)
id = prefix + ' ' + value;
});
if (parameters.subtitle) {
id = (id ? id + ': ' : '') + parameters.subtitle;
}
// console.trace(id);
}
if (id) {
return '<td id="' + wiki_API.plain_text(id) + '"></td>';
}
}
function parse_template_Episode_table__part(template_token, index, parent,
options) {
template_token.expand = expand_template_Episode_table__part;
}
// --------------------------------------------------------------------------------------------
function parse_template_Pin_message(template_token, index, parent, options) {
var parameters = template_token.parameters, message_expire_date;
if (parameters[1]) {
options = library_namespace.new_options(options);
options.get_timevalue = true;
message_expire_date = wiki_API.parse.date(parameters[1], {
get_timevalue : true,
});
}
// console.trace([ message_expire_date, parameters ]);
template_token.message_expire_date = message_expire_date || Infinity;
}
// --------------------------------------------------------------------------------------------
// 有缺陷的簡易型 Lua patterns to JavaScript RegExp
function Lua_pattern_to_RegExp_pattern(pattern) {
return String(pattern).replace(/%l/g, 'a-z').replace(/%u/g, 'A-Z')
// e.g., %d, %s, %S, %w, %W
.replace(/%/g, '\\');
}
// https://en.wikipedia.org/wiki/Module:Check_for_unknown_parameters
function check_template_for_unknown_parameters(template_token, options) {
var valid_parameters = this.valid_parameters, valid_RegExp_parameters = this.valid_RegExp_parameters;
var invalid_parameters = Object.keys(template_token.parameters)
//
.filter(function() {
if (valid_parameters.has(parameter))
return;
return !valid_RegExp_parameters.some(function(pattern) {
return pattern.test(parameter);
});
}, this);
if (invalid_parameters.length === 0) {
return;
}
var return_value = {
invalid_parameters : invalid_parameters
};
var unknown_text = this.parameters.unknown || 'Found _VALUE_, ';
var preview_text = this.parameters.preview;
unknown_text = invalid_parameters.map(function(parameter) {
return unknown_text.replace(/_VALUE_/g, parameter);
}).join('').replace(/[,\s]+$/, '');
if (preview_text) {
preview_text = invalid_parameters.map(function(parameter) {
return preview_text.replace(/_VALUE_/g, parameter);
}).join('').replace(/[,\s]+$/, '');
}
return {
invalid_parameters : invalid_parameters,
preview_text : preview_text || unknown_text,
unknown_text : unknown_text
};
}
function parse_module_Check_for_unknown_parameters(token, index, parent,
options) {
var parameters = token.parameters, valid_parameters = token.valid_parameters = new Set, valid_RegExp_parameters = token.valid_RegExp_parameters = [];
for (var index = 1; index < token.length; index++) {
var value = parameters[index];
if (value)
valid_parameters.add(String(value));
if (value = parameters['regexp' + index]) {
try {
value = new RegExp('^'
+ Lua_pattern_to_RegExp_pattern(value) + '$');
valid_RegExp_parameters.push(value);
} catch (e) {
library_namespace.error([
//
'parse_module_Check_for_unknown_parameters: ', {
T : [
// gettext_config:{"id":"cannot-convert-lua-pattern-to-regexp-pattern-$1"}
'Cannot convert Lua pattern to RegExp pattern: %1',
//
value ]
} ]);
}
}
}
token.check_template = check_template_for_unknown_parameters
.bind(token);
}
// --------------------------------------------------------------------------------------------
function expand_module_IPAddress(options) {
// console.trace(this);
var parameters = this.parameters;
// console.trace(parameters);
if (this.function_name === 'isIp') {
// [ , 'IPAddress', 'isIp', '...' ]
var is_IP = library_namespace.is_IP(parameters[1]);
return is_IP ? String(is_IP) : '';
}
// TODO:
}
function parse_module_IPAddress(token, index, parent, options) {
token.expand = expand_module_IPAddress;
}
// --------------------------------------------------------------------------------------------
function expand_template_Template_link(options) {
var parameters = this.parameters;
return '&#123;&#123;[[Template:' + parameters[1] + '|' + parameters[1]
+ ']]&#125;&#125;';
}
function parse_template_Template_link(token, index, parent, options) {
token.expand = expand_template_Template_link;
}
// --------------------------------------------------------------------------------------------
// export 導出.
// general_functions 必須在個別 wiki profiles 之前載入。
// 如 CeL.application.net.wiki.template_functions.jawiki 依賴於
// general_functions
wiki_API.template_functions.functions_of_all_sites = {
Void : parse_template_Void,
Center : parse_template_Center,
// 一些會用於章節標題的特殊模板。 for preprocess_section_link_token()
Font : parse_template_Font,
Color : parse_template_Color,
'Colored link' : parse_template_Colored_link,
'@' : parse_template_At,
'User link' : parse_template_User_link,
'Module:If empty' : parse_module_If_empty,
// 一些會添加 anchors 的特殊模板。
// 會生成網頁錨點的模板或模組。
// Templates or modules that generate web anchors
Anchor : parse_template_Anchor,
'Module:Anchor' : parse_template_Anchor,
'Visible anchor' : parse_template_Visible_anchor,
Term : parse_template_Term,
Wikicite : parse_template_Wikicite,
// Sfn : parse_template_Sfn,
SfnRef : parse_template_SfnRef,
'Episode table' : parse_template_Episode_table,
'Episode table/part' : parse_template_Episode_table__part,
'Episode list' : parse_template_Episode_list,
'Episode list/sublist' : parse_template_Episode_list,
// wiki/routine/20210429.Auto-archiver.js: avoid being archived
'Pin message' : parse_template_Pin_message,
'Module:Check for unknown parameters' : parse_module_Check_for_unknown_parameters,
'Module:IPAddress' : parse_module_IPAddress,
// TODO
// 'Module:Unsubst' : parse_module_Unsubst,
// 'Template link'
Tl : parse_template_Template_link
};
// --------------------------------------------------------------------------------------------
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

View File

@@ -0,0 +1,161 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): 常用模板特設功能。本工具檔放置的是指定 wiki
* 計畫特有的模板。
*
* 注意: 本程式庫必須應各 wiki project 模板內容改動而改寫。
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
</code>
*
* @since 2021/1/24 16:6:50
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.template_functions.jawiki',
require : 'data.native.'
// Should also load essential MediaWiki modules
+ '|application.net.wiki.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki;
// @inner
// var is_api_and_title = wiki_API.is_api_and_title,
// normalize_title_parameter = wiki_API.normalize_title_parameter;
var to_exit = wiki_API.parser.parser_prototype.each.exit;
// e.g., 'zhwiki'
var module_site_name = this.id.match(/[^.]+$/)[0];
function empty_string(/* options */) {
// var token = this;
return '';
}
// --------------------------------------------------------------------------------------------
// token.expand() 可將模板轉換成一般 wiki 語法。
// https://www.mediawiki.org/w/api.php?action=help&modules=expandtemplates
// 用於 function preprocess_section_link_token()。
// --------------------------------------------------------------------------------------------
function expand_template_Enlink(options) {
var parameters = this.parameters;
var lang = parameters[3] || 'en';
var wikitext;
if (parameters.a === 'on') {
wikitext = lang;
} else {
wikitext = (parameters[3] ? parameters[3] + ':' : '')
+ (parameters[2] || parameters[1]);
if (parameters.i === 'on')
wikitext = "''" + wikitext + "''";
}
wikitext = '[[:' + lang + ':' + parameters[1]
//
+ '|' + wikitext + ']]';
if (!parameters.p || parameters.p === 'on')
wikitext = '&nbsp;(' + wikitext + '&nbsp;';
if (!parameters.s || parameters.s === 'on')
wikitext = '<small>' + wikitext + '</small>';
return ' (' + parameters[1] + ')';
}
function parse_template_Enlink(token) {
token.expand = expand_template_Enlink;
}
// --------------------------------------------------------------------------------------------
function expand_template_to_display_language(options) {
// console.trace(this.toString());
var parameters = this.parameters;
return parameters[1];
}
function parse_template_to_display_language(token) {
token.expand = expand_template_to_display_language;
}
// --------------------------------------------------------------------------------------------
function expand_template_拡張漢字(options) {
var parameters = this.parameters;
return parameters[2] || parameters[1];
}
function parse_template_拡張漢字(token) {
token.expand = expand_template_拡張漢字;
}
// --------------------------------------------------------------------------------------------
// Not completed! Only for get_all_anchors().
// 転送先のアンカーはTemplate:RFDの中に納まっている
// e.g., {{RFD notice
// |'''対象リダイレクト:'''[[Wikipedia:リダイレクトの削除依頼/受付#RFD長崎市電|長崎市電(受付依頼)]]|...}}
function expand_template_RFD(options) {
var parameters = this.parameters;
// {{RFD|リダイレクト元ページ名|リダイレクト先ページ名}}
return '<span id="RFD' + parameters[1] + '"></span>'
// TODO: + ...
;
}
function parse_template_RFD(token) {
token.expand = expand_template_RFD;
}
// --------------------------------------------------------------------------------------------
// export 導出.
wiki_API.template_functions.functions_of_site[module_site_name] = {
// 一些會添加 anchors 的特殊模板。
Anchors : wiki_API.template_functions.functions_of_all_sites.Anchor,
Enlink : parse_template_Enlink,
ARIB外字フォント : parse_template_to_display_language,
CP932フォント : parse_template_to_display_language,
JIS90フォント : parse_template_to_display_language,
JIS2004フォント : parse_template_to_display_language,
MacJapanese : parse_template_to_display_language,
変体仮名フォント : parse_template_to_display_language,
絵文字フォント : parse_template_to_display_language,
補助漢字フォント : parse_template_to_display_language,
通貨フォント : parse_template_to_display_language,
拡張漢字 : parse_template_拡張漢字,
RFD : parse_template_RFD
};
// --------------------------------------------------------------------------------------------
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

View File

@@ -0,0 +1,210 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): 常用模板特設功能。本工具檔放置的是指定 wiki
* 計畫特有的模板。
*
* 注意: 本程式庫必須應各 wiki project 模板內容改動而改寫。
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
</code>
*
* @since 2021/1/24 16:6:50
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.template_functions.zhmoegirl',
require : 'data.native.'
// Should also load essential MediaWiki modules
+ '|application.net.wiki.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki;
// @inner
// var is_api_and_title = wiki_API.is_api_and_title,
// normalize_title_parameter = wiki_API.normalize_title_parameter;
var to_exit = wiki_API.parser.parser_prototype.each.exit;
// e.g., 'zhwiki'
var module_site_name = this.id.match(/[^.]+$/)[0];
function empty_string(/* options */) {
// var token = this;
return '';
}
// --------------------------------------------------------------------------------------------
// token.expand() 可將模板轉換成一般 wiki 語法。
// https://www.mediawiki.org/w/api.php?action=help&modules=expandtemplates
// 用於 function preprocess_section_link_token()。
// --------------------------------------------------------------------------------------------
// for get_all_anchors()
function expand_template_A(options) {
var parameters = this.parameters;
// {{a|锚点名称|显示文字}}
return '<span id="' + parameters[1] + '">'
+ (parameters[2] || parameters[1]) + '</span>';
}
function parse_template_A(token, index, parent, options) {
token.expand = expand_template_A;
}
// --------------------------------------------------------------------------------------------
// [[Module:Ruby]]
function expand_module_Ruby(parameters) {
// converted wikitext
var wikitext = [];
wikitext.push('<ruby'
+ (parameters.id ? ' id="' + parameters.id + '"' : '') + '>');
wikitext.push('<rb'
+ (parameters.rbid ? ' id="' + parameters.rbid + '"' : '')
+ '>' + (parameters[1] || '') + '</rb>');
wikitext.push('');
wikitext.push('<rt'
+ (parameters.rtid ? ' id="' + parameters.rtid + '"' : '')
+ '>' + (parameters[2] || '') + '</rt>');
wikitext.push('');
wikitext.push('</ruby>');
return wikitext.join('');
}
// for get_all_anchors()
function expand_template_Ruby(options) {
var parameters = this.parameters;
// {{Ruby|文字|注音|文字的語言标签|注音的語言标签}}
return expand_module_Ruby(parameters);
}
function parse_template_Ruby(token, index, parent, options) {
token.expand = expand_template_Ruby;
}
// --------------------------------------------------------------------------------------------
// for preprocess_section_link_token()
function expand_template_Dead(options) {
var parameters = this.parameters;
return parameters[1];
}
function parse_template_Dead(token) {
token.expand = expand_template_Dead;
}
// --------------------------------------------------------------------------------------------
// for preprocess_section_link_token()
function expand_template_黑幕(options) {
var parameters = this.parameters;
return parameters[1];
}
function parse_template_黑幕(token) {
token.expand = expand_template_黑幕;
}
// --------------------------------------------------------------------------------------------
// for preprocess_section_link_token()
// {{Lj|...}} 是日語{{lang|ja|...}}的縮寫 @ zh.moegirl
function expand_template_Lj(options) {
var parameters = this.parameters;
return '-{' + parameters[1] + '}-';
}
function parse_template_Lj(token) {
token.expand = expand_template_Lj;
}
// --------------------------------------------------------------------------------------------
// Not completed! Only for get_all_anchors() @ [[ACGN作品中出場的鐵路車站列表]]
function expand_template_铁路车站名(options) {
var parameters = this.parameters;
return '<span id="' + (parameters.name || parameters[1]) + '">'
// TODO: The content is skipped.
+ '</span>';
}
function parse_template_铁路车站名(token) {
token.expand = expand_template_铁路车站名;
}
// --------------------------------------------------------------------------------------------
// Not completed! Only for get_all_anchors() as section title
// @ [[ARGONAVIS from BanG Dream! 翻唱曲列表]]
function expand_template_ARGONAVIS_Icon(options) {
// TODO: The content is skipped.
return '';
}
function parse_template_ARGONAVIS_Icon(token) {
token.expand = expand_template_ARGONAVIS_Icon;
}
// --------------------------------------------------------------------------------------------
// Not completed! Only for get_all_anchors()
// @ zh.moegirl [[FLOWERS(Innocent Grey)]]
function expand_template_Gradient_Text(options) {
var parameters = this.parameters;
// {{Gradient_Text|漸變色代碼|文字內容|title=鼠標懸停在文字上顯示的注釋}}
return parameters[2] || '';
}
function parse_template_Gradient_Text(token) {
token.expand = expand_template_Gradient_Text;
}
// --------------------------------------------------------------------------------------------
// export 導出.
wiki_API.template_functions.functions_of_site[module_site_name] = {
// 一些會添加 anchors 的特殊模板。
A : parse_template_A,
Ruby : parse_template_Ruby,
铁路车站名 : parse_template_铁路车站名,
// 一些會用於章節標題的特殊模板。 for preprocess_section_link_token()
Dead : parse_template_Dead,
黑幕 : parse_template_黑幕,
Lj : parse_template_Lj,
'ARGONAVIS/Icon' : parse_template_ARGONAVIS_Icon,
'Gradient Text' : parse_template_Gradient_Text
};
// library_namespace.info(module_site_name + ': 採用 zhwiki 的模板特設功能設定。');
wiki_API.template_functions.functions_of_site[module_site_name][wiki_API.template_functions.KEY_dependent_on] = [ 'zhwiki' ];
// --------------------------------------------------------------------------------------------
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

View File

@@ -0,0 +1,387 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): 常用模板特設功能。本工具檔放置的是指定 wiki
* 計畫特有的模板。
*
* 注意: 本程式庫必須應各 wiki project 模板內容改動而改寫。
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
</code>
*
* @since 2021/1/24 16:6:50
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// @examples
(function() {
require('./wiki loader.js');
CeL.run('application.net.wiki.template_functions');
var wiki = Wiki(true, 'zh');
wiki.page('簡繁轉換一對多列表').parse(function(parsed) {
// var page_data = parsed.page;
parsed.each('Template:簡繁轉換', function(token) {
console.log(token. + '⇄' + token.);
});
});
});
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.template_functions.zhwiki',
require : 'data.native.'
// Should also load essential MediaWiki modules
+ '|application.net.wiki.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki;
// @inner
// var is_api_and_title = wiki_API.is_api_and_title,
// normalize_title_parameter = wiki_API.normalize_title_parameter;
var to_exit = wiki_API.parser.parser_prototype.each.exit;
// e.g., 'zhwiki'
var module_site_name = this.id.match(/[^.]+$/)[0];
function empty_string(/* options */) {
// var token = this;
return '';
}
// --------------------------------------------------------------------------------------------
// token.expand() 可將模板轉換成一般 wiki 語法。
// https://www.mediawiki.org/w/api.php?action=help&modules=expandtemplates
// 用於 function preprocess_section_link_token()。
// --------------------------------------------------------------------------------------------
function expand_template_A(options) {
var parameters = this.parameters;
return (parameters.name ? '<span id="' + parameters.name
//
+ '"></span>' : '') + '[[' + parameters[1]
//
+ (parameters[2] ? '|' + parameters[2] : '') + ']]';
}
// --------------------------------------------------------------------------------------------
// [[w:zh:Template:Al]]
function expand_template_Al(options) {
var token = this;
return token.page_title_list.map(function(title) {
return wiki_API.title_link_of(title);
}).join('、');
}
function parse_template_Al(token, index, parent, options) {
var index = 0, page_title_list = [];
while (index < token.length) {
var page_title = token.parameters[++index];
// allow `{{al||title}}`
if (page_title)
page_title_list.push(page_title);
}
Object.assign(token, {
page_title_list : page_title_list,
expand : expand_template_Al
});
return page_title_list;
}
// --------------------------------------------------------------------------------------------
function parse_template_不存檔(token, index, parent, options) {
token.message_expire_date = Infinity;
}
// --------------------------------------------------------------------------------------------
function expand_template_楷體(options) {
var parameters = this.parameters;
return '<span class="template-kai">' + (parameters[1] || '楷体')
+ '</span>';
}
function parse_template_楷體(token, index, parent, options) {
token.expand = expand_template_楷體;
}
// --------------------------------------------------------------------------------------------
/**
* [[Template:Interlanguage link]] 跨語言模板 多語言模板。會為 token 增加下列屬性: <code>
</code>
*/
var interlanguage_link_template_attributes = {
// local_title: local title 中文條目名
"local_page_title" : '',
// 只會提供第一個。
"foreign_language_code" : '',
// 只會提供第一個。
"foreign_page_title" : '',
"foreign_page_mapper" : {
// foreign_language: foreign language code 外文語言代號
// foreign_title: foreign title 外文條目名
foreign_language_code : 'foreign_page_title'
},
// label: label text displayed 顯示名
"display_text" : '',
// Keep foreign language links when displayed
"preserve_foreign_links" : true,
"wikidata_entity_id" : '' || 1,
// 屬性的index改變屬性值時使用。
"attribute_index" : {
"local_page_title" : 1,
// 只會提供第一個。
"foreign_language_code" : 1,
// 只會提供第一個。
"foreign_page_title" : 1,
"display_text" : 1,
"preserve_foreign_links" : 1,
"wikidata_entity_id" : 1
}
};
function setup_interlanguage_link_template_parameters(template_pattern) {
var parsed_token = wiki_API.parse(template_pattern);
var attribute_index = Object.create(null);
var configuration = {
attribute_index : attribute_index
};
var parameters = parsed_token.parameters;
for ( var parameter in parameters) {
var attribute_name = parameters[parameter];
if (attribute_name in interlanguage_link_template_attributes)
attribute_index[attribute_name] = parameter;
}
var functions_of_site = wiki_API.template_functions.functions_of_site[module_site_name];
if (functions_of_site[parsed_token.name]) {
library_namespace
.error('setup_interlanguage_link_template_parameters: '
+ '已設定' + parsed_token.name
+ '之模板特設功能,無法設定跨語言模板功能。');
return;
}
functions_of_site[parsed_token.name] = parse_interlanguage_link_template
.bind(configuration);
}
function parse_interlanguage_link_template(token, index, parent, options) {
var configuration = this;
var attribute_index = configuration.attribute_index;
var foreign_page_mapper = Object.create(null);
for ( var attribute_name in attribute_index) {
if (attribute_index[attribute_name] in token.parameters)
token[attribute_name] = token.parameters[attribute_index[attribute_name]];
}
if ('foreign_language_code' in token)
foreign_page_mapper[token.foreign_language_code] = token.foreign_page_title;
token.attribute_index = attribute_index;
token.foreign_page_mapper = foreign_page_mapper;
}
// --------------------------------------------------------------------------------------------
// {{Lang|ja|參數值}} → -{參數值}-
function expand_template_Lang(options) {
var parameters = this.parameters;
return /^(?:zh|gan)/.test(parameters[1]) ? parameters[2] : '-{'
+ parameters[2] + '}-';
}
function parse_template_Lang(token, index, parent, options) {
token.expand = expand_template_Lang;
}
// --------------------------------------------------------------------------------------------
// [[w:zh:Template:NoteTA]]
function parse_template_NoteTA(token, options) {
var conversion_list = Object.assign([], {
// 固定轉換規則
// fixed : [],
// 公共轉換組
group_data : [],
groups : []
});
var index, value = token.parameters.T;
if (value) {
// 標題轉換
conversion_list.title = value;
}
// TODO: {{NoteTA}} 使用「1=」可以同時轉換標題和正文(T=)
for (index = 1; index < token.length; index++) {
value = token.parameters[index];
if (!value)
continue;
// [[w:zh:模組:NoteTA]]
// @see function item_to_conversion(item) @
// CeL.application.net.wiki
value = wiki_API.parse('-{A|' + value + '}-', {
normalize : true,
with_properties : true
});
if (typeof value === 'string') {
// 遇到無法轉換的值別 throw。 e.g., "a\nb"
continue;
}
// value.parameter_name = index;
value.index = token.index_of[index];
// console.log(value);
conversion_list.push(value);
}
// [[w:zh:Module:NoteTA]]
for (index = 1; index < token.length; index++) {
var parameter_name = 'G' + index;
value = token.parameters[parameter_name];
if (!value)
continue;
value = wiki_API.parse.wiki_token_to_key(value);
// console.trace(value);
if (typeof value === 'string') {
value = value.replace(/_/g, ' ').trim();
} else {
library_namespace.warn('parse_template_NoteTA: 非字串之公共轉換組名稱: ['
+ value + '] @ ' + token);
console.trace(value);
}
conversion_list.groups.push(value.toString());
conversion_list.group_data[value.toString()] = {
parameter_name : parameter_name,
group_name : value,
index : token.index_of[parameter_name]
};
// TODO
}
Object.assign(token, {
conversion_list : conversion_list,
expand : empty_string
});
return conversion_list;
}
// --------------------------------------------------------------------------------------------
function template_簡繁轉換_to_string(template_token, parameter) {
var words = template_token.parameters[parameter];
if (Array.isArray(words)) {
words = words.map(function(token) {
if (typeof token === 'string')
return token;
if (token.tag === 'sup') {
// e.g., "<sup>台/陸繁</sup>"
return '';
}
if (token.type === 'transclusion') {
if (token.name === 'Lang'
//
&& typeof token.parameters[2] === 'string')
return token.parameters[2];
if (token.name === '僻字') {
// console.log(token.toString());
}
if (token.name === '僻字'
//
&& typeof token.parameters[1] === 'string')
return token.parameters[1];
}
throw new Error('包含無法處理的字元: ' + token);
}).join('');
}
words = library_namespace.HTML_to_Unicode(words);
// [[w:zh:Unicode字符平面映射]]
// http://ubuntu-rubyonrails.blogspot.com/2009/06/unicode.html
words = words.replace(
// 發音用 Pinyin diacritic-vowel combinations:
// \u00E0-\u00FC [[w:en:Latin-1 Supplement (Unicode block)]]
// \u0100-\u017F [[w:en:Latin Extended-A]]
// \u01CD-\u01DC [[w:en:Latin Extended-B]]
/[、a-z\u00E0-\u00FC\u0100-\u017F\u01CD-\u01DC\uD800-\uDFFF]/g, '');
if (false && /[^\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u2E80-\u2EFF]/
.test(words)) {
// words.charCodeAt(0).toString(16)
console.log([ words, words.replace(
// 匹配中文字符的正則表達式
/[\u4E00-\u9FFF\u3400-\u4DBF\uF900-\uFAFF\u2E80-\u2EFF]/g,
//
'') ]);
// throw words;
}
return words;
}
// for {{簡繁轉換}} @ [[w:zh:簡繁轉換一對多列表]]
// @see wiki_API.convert_Chinese()
function parse_template_簡繁轉換(token) {
Object.assign(token, {
: template_簡繁轉換_to_string(token, 's'),
: template_簡繁轉換_to_string(token, 't')
});
}
// --------------------------------------------------------------------------------------------
// export 導出.
wiki_API.template_functions.functions_of_site[module_site_name] = {
// 一些會用於章節標題的特殊模板。 for preprocess_section_link_token()
A : {
properties : {
expand : expand_template_A
}
},
Al : parse_template_Al,
// {{Do not archive}}
// wiki/routine/20210429.Auto-archiver.js: avoid being archived
不存檔 : parse_template_不存檔,
// [[Template:Interlanguage link]] 跨語言模板 多語言模板。
Lang : parse_template_Lang,
NoteTA : parse_template_NoteTA,
簡繁轉換 : parse_template_簡繁轉換
};
[ '{{Interlanguage link multi|local_page_title|foreign_language_code|foreign_page_title|lt=display_text|WD=wikidata_entity_id}}' ]
.forEach(setup_interlanguage_link_template_parameters);
// --------------------------------------------------------------------------------------------
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

View File

@@ -0,0 +1,85 @@
/**
* @name CeL function for MediaWiki (Wikipedia / 維基百科): 常用模板特設功能。本工具檔放置的是指定 wiki
* 計畫特有的模板。
*
* 注意: 本程式庫必須應各 wiki project 模板內容改動而改寫。
*
* @fileoverview 本檔案包含了 MediaWiki 自動化作業用程式庫的子程式庫。
*
* TODO:<code>
</code>
*
* @since 2021/11/19 5:4:8
*/
// More examples: see /_test suite/test.js
// Wikipedia bots demo: https://github.com/kanasimi/wikibot
'use strict';
// 'use asm';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.wiki.template_functions.zhwiktionary',
require : 'data.native.'
// Should also load essential MediaWiki modules
+ '|application.net.wiki.',
// 設定不匯出的子函式。
no_extend : 'this,*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
var wiki_API = library_namespace.application.net.wiki;
// @inner
// var is_api_and_title = wiki_API.is_api_and_title,
// normalize_title_parameter = wiki_API.normalize_title_parameter;
var to_exit = wiki_API.parser.parser_prototype.each.exit;
// e.g., 'zhwiktionary'
var module_site_name = this.id.match(/[^.]+$/)[0];
function empty_string(/* options */) {
// var token = this;
return '';
}
// --------------------------------------------------------------------------------------------
// token.expand() 可將模板轉換成一般 wiki 語法。
// https://www.mediawiki.org/w/api.php?action=help&modules=expandtemplates
// 用於 function preprocess_section_link_token()。
// --------------------------------------------------------------------------------------------
function expand_template_語言標題(options) {
var parameters = this.parameters;
return '\n==' + (parameters.l || parameters. || parameters.) + '==';
}
function parse_template_語言標題(token, index, parent, options) {
token.expand = expand_template_語言標題;
}
// --------------------------------------------------------------------------------------------
// export 導出.
wiki_API.template_functions.functions_of_site[module_site_name] = {
語言標題 : parse_template_語言標題,
};
// --------------------------------------------------------------------------------------------
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

745
app/node_modules/cejs/application/net/work_crawler.js generated vendored Normal file
View File

@@ -0,0 +1,745 @@
/**
* @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');
}
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;
// ------------------------------------------
// 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;
text_list = text_list.filter(function(text) {
return text && text.trim()
//
&& !(text in _this.converted_text_cache);
});
if (text_list.length === 0) {
// Already cached all text needed.
return;
}
// console.trace(text_list.length + ' text to be converted.');
if (initializated) {
return this.convert_text_language_using(text_list, options)
// assert: .convert_text_language_using() return thenable
.then(function set_text_list(converted_text_list) {
text_list.forEach(function(text, index) {
_this.converted_text_cache[text]
//
= converted_text_list[index];
});
// console.trace(_this.converted_text_cache);
});
}
// 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;
}).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') {
delete this.converted_text_cache[options.text];
} else {
// console.trace(options);
delete this.converted_text_cache;
}
if (options.including_persistence)
delete this.converted_text_cache_persisted;
}
function convert_text_language(text, 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
if (text in this.converted_text_cache) {
var converted_text = this.converted_text_cache[text];
if (false && text.length !== converted_text.length) {
throw new Error('Different length:\n' + text + '\n'
+ converted_text);
}
if (options && options.persistence)
this.converted_text_cache_persisted[text] = converted_text;
return converted_text;
}
if (text in this.converted_text_cache_persisted) {
return this.converted_text_cache_persisted[text];
}
if (options && options.allow_non_cache) {
return text;
}
// console.trace(this.converted_text_cache);
// console.trace(text);
// 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
&& !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,
null_XMLHttp : {
responseText : ''
}
});
return Work_crawler;
}

View File

@@ -0,0 +1,51 @@
= CeJS 網路作品爬蟲程式庫 =
批量下載網路作品(小說、漫畫)的函式庫。 WWW work crawler library.
More examples: See [https://github.com/kanasimi/work_crawler 各網站工具檔.js]
== 下載作業流程 ==
[[../work_crawler.js]]: [[arguments.js]]
→ [[task.js]]
→ [[search.js]]
→ [[work.js]]
→ [[chapter.js]]
→ [[image.js]] or [[ebook.js]]
# 獲取伺服器列表。 start_downloading()
# 解析設定檔,判別所要下載的作品列表。 parse_work_id(), get_work_list(), .base_URL, .extract_work_id()
# 特別處理特定id。 .convert_id()
# 解析 作品名稱 → 作品id get_work(), .search_URL, .parse_search_result()
# 獲取作品資訊與各章節資料。 get_work_data(), pre_process_chapter_list_data(), process_chapter_list_data()
# 對於章節列表與作品資訊分列不同頁面(URL)的情況,應該另外指定 .chapter_list_URL。 get_work_data(), .work_URL, .parse_work_data(), chapter_list_URL, .get_chapter_list(), .after_get_work_data()
# 獲取每一個章節的內容與各個影像資料。 pre_get_chapter_data(), .chapter_URL, get_chapter_data(), .pre_parse_chapter_data(), .parse_chapter_data()
# 獲取各個章節的每一個影像內容。 get_image(), .image_preprocessor(), .image_post_processor(), .after_get_image()
# finish_up(), .after_download_chapter(), .after_download_work()
== History ==
{| class="wikitable"
|+ History 沿革
! Date !! Modify
|-
| 2016/10/30 21:40:6 || 完成主要架構設計與構思,開始撰寫程式。
|-
| 2016/11/1 23:15:16 || 正式運用:批量下載腾讯漫画 qq。
|-
| 2016/11/5 22:44:17 || 正式運用:批量下載漫画台 manhuatai。
|-
| 2016/11/27 19:7:2 || 模組化。 ([[sites]]/*)
|-
| 2019/10/13 13:23:25 || 分拆至 work_crawler/*.js
|}
== See also ==
* https://github.com/abc9070410/JComicDownloader
* http://pxer.pea3nut.org/md/use https://github.com/eight04/ComicCrawler
* https://github.com/riderkick/FMD https://github.com/yuru-yuri/manga-dl
* https://github.com/Xonshiz/comic-dl
* https://github.com/wellwind/8ComicDownloaderElectron
* https://github.com/inorichi/tachiyomi
* https://github.com/Arachnid-27/Cimoc
* https://github.com/qq573011406/KindleHelper
* https://github.com/InzGIBA/manga
* [https://scrapy.org/ Scrapy 爬蟲框架]

View File

@@ -0,0 +1,540 @@
/**
* @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',
one_by_one : 'boolean',
// 篩選想要下載的章節標題關鍵字。例如"單行本"。
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',
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',
recheck : 'boolean|string:changed;multi_parts_changed',
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;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,665 @@
/**
* @name WWW work crawler sub-functions
*
* @fileoverview WWW work crawler functions: part of image
*
* @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.image',
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, crawler_namespace = Work_crawler.crawler_namespace;
var gettext = library_namespace.locale.gettext,
/** node.js file system module */
node_fs = library_namespace.platform.nodejs && require('fs');
// --------------------------------------------------------------------------------------------
function image_path_to_url(path, server) {
if (path.includes('://')) {
return path;
}
if (!server.includes('://')) {
// this.get_URL_options.headers.Host = server;
server = 'http://' + server;
}
return server + path;
}
function EOI_error_path(path, XMLHttp) {
return path.replace(/(\.[^.]*)$/, this.EOI_error_postfix
// + (XMLHttp && XMLHttp.status ? ' ' + XMLHttp.status : '')
+ '$1');
}
// 下載單一個圖片。
// callback(image_data, status)
function get_image(image_data, callback, images_archive) {
// console.log(image_data);
if (!image_data || !image_data.file || !image_data.url) {
if (image_data) {
image_data.has_error = true;
image_data.done = true;
}
// 注意: 此時 image_data 可能是 undefined
if (this.skip_error) {
// gettext_config:{"id":"unspecified-image-data"}
this.onwarning(gettext('未指定圖片資料'), image_data);
} else {
// gettext_config:{"id":"unspecified-image-data"}
this.onerror(gettext('未指定圖片資料'), image_data);
}
if (typeof callback === 'function')
callback(image_data, 'invalid_data');
return;
}
/**
* 每張圖片都要檢查實際存在的圖片檔案。當之前已存在完整的圖片時,就不再嘗試下載圖片。<br />
* 工作機制:<br />
* 檢核`image_data.file`是否存在。`image_data.file`由圖片的網址URL來判別可能的延伸檔名。猜不出的會採用預設的圖片延伸檔名/副檔名.default_image_extension。
*
* @see function process_images() @ CeL.application.net.work_crawler.chapter
*
* 若`image_data.file`不存在,將會檢核所有可接受的圖片類別副檔名(.acceptable_types)。
* 每張圖片都要檢核所有可接受的圖片類別,會加大硬碟讀取負擔。 會用到 .overwrite_old_file 這個選項的,應該都是需要提報
* issue 的,因此這個選項不會列出來。麻煩請在個別網站遇到此情況時提報 issue列出作品名稱以及圖片類別以供這邊確認圖片類別。
* 只要存在完整無損害的預設圖片類別或是可接受的圖片類別,就直接跳出,不再嘗試下載這張圖片。否則會重新下載圖片。
* 當下載的圖片以之前的圖片更大時,就會覆蓋原先的圖片。
* 若下載的圖片類別並非預設的圖片類別(.default_image_extension),例如預設 JPG 但得到 PNG
* 檔案時,會將副檔名改為實際得到的圖像格式。因此下一次下載時,需要設定 .acceptable_types 才能找得到圖片。
*/
var image_downloaded = node_fs.existsSync(image_data.file)
|| this.skip_existed_bad_file
// 檢查是否已有上次下載失敗,例如 server 上本身就已經出錯的檔案。
&& node_fs.existsSync(this.EOI_error_path(image_data.file)), acceptable_types;
if (!image_downloaded) {
// 正規化 acceptable_types
acceptable_types = image_data.acceptable_types
|| this.acceptable_types;
if (!acceptable_types) {
// 未設定將不作檢查。
} else if (typeof acceptable_types === 'string') {
acceptable_types = acceptable_types.trim();
if (acceptable_types === 'images') {
// 將會測試是否已經下載過一切可接受的檔案類別。
acceptable_types = Object.keys(this.image_types);
} else {
acceptable_types = acceptable_types.split('|');
}
} else if (!Array.isArray(acceptable_types)) {
library_namespace.warn({
// gettext_config:{"id":"invalid-acceptable_types-$1"}
T : [ 'Invalid acceptable_types: %1', acceptable_types ]
});
acceptable_types = null;
}
if (acceptable_types) {
// 檢核所有可接受的圖片類別(.acceptable_types)。
image_downloaded = acceptable_types.some(function(extension) {
var alternative_filename = image_data.file.replace(
/\.[a-z\d]+$/, '.' + extension);
if (node_fs.existsSync(alternative_filename)) {
image_data.file = alternative_filename;
return true;
}
});
}
}
// 檢查壓縮檔裡面的圖片檔案。
var image_archived, bad_image_archived;
if (images_archive && images_archive.fso_path_hash
// 檢查壓縮檔,看是否已經存在圖像檔案。
&& image_data.file.startsWith(images_archive.work_directory)) {
image_archived = image_data.file
.slice(images_archive.work_directory.length);
bad_image_archived = images_archive.fso_path_hash[this
.EOI_error_path(image_archived)];
if (image_archived && bad_image_archived) {
images_archive.to_remove.push(bad_image_archived);
}
if (false) {
console.log([ images_archive.fso_path_hash, acceptable_types,
image_archived,
images_archive.fso_path_hash[image_archived] ]);
}
image_downloaded = image_downloaded
|| images_archive.fso_path_hash[image_archived]
|| this.skip_existed_bad_file
// 檢查是否已有上次下載失敗,例如 server 上本身就已經出錯的檔案。
&& bad_image_archived;
if (!image_downloaded
// 可以接受的圖片類別/圖片延伸檔名/副檔名/檔案類別 acceptable file extensions
&& acceptable_types) {
image_downloaded = acceptable_types.some(function(extension) {
var alternative_filename = image_archived.replace(
/\.[a-z\d]+$/, '.' + extension);
return images_archive.fso_path_hash[alternative_filename];
});
}
}
if (image_downloaded) {
// console.log('get_image: Skip ' + image_data.file);
image_data.done = true;
if (typeof callback === 'function')
callback(image_data, 'image_downloaded');
return;
}
// --------------------------------------
var _this = this,
// 漫畫圖片的 URL。
image_url = image_data.url, server = this.server_list;
if (server) {
server = server[server.length * Math.random() | 0];
image_url = this.image_path_to_url(image_url, server, image_data);
} else {
image_url = this.full_URL(image_url);
}
image_data.parsed_url = image_url;
if (!crawler_namespace.PATTERN_non_CJK.test(image_url)) {
// 工具檔應先編碼URL。
library_namespace.warn({
// gettext_config:{"id":"invalid-url-must-encode-first-$1"}
T : [ '必須先將URL編碼%1', image_url ]
});
// console.trace(image_url);
if (!/%[\dA-F]{2}/i.test(image_url))
image_url = encodeURI(image_url);
}
if (!image_data.file_length) {
image_data.file_length = [];
}
// console.log('get_image: image_url: ' + image_url);
// library_namespace.set_debug(3);
this.get_URL(image_url, function(XMLHttp) {
// console.trace(XMLHttp.status);
// console.log(image_data);
if (image_data.url !== XMLHttp.responseURL) {
// 紀錄最後實際下載的圖片網址。
image_data.responseURL = XMLHttp.responseURL;
}
/** {Buffer}圖片數據的內容。 */
var contents = XMLHttp.buffer;
// 修正圖片結尾 tail 非正規格式之情況。
// TODO: 應該檢測刪掉後是正確的圖片檔,才刪掉 trailing new line。
if (_this.trim_trailing_newline && contents && contents.length > 4
// 去掉最後的換行符號:有些圖片在檔案最後會添加上換行符號 "\r\n",因此被判別為非正規圖片檔。
&& contents.at(-2) === 0x0D && contents.at(-1) === 0x0A) {
contents = contents.slice(0, -2);
}
if (_this.image_preprocessor) {
// 圖片前處理程序 預處理器 image pre-processing
// 例如修正圖片結尾非正規格式之情況。
// 必須自行確保不會 throw需檢查 contents 是否非 {Buffer}。
try {
contents = _this.image_preprocessor(contents, image_data);
} catch (e) {
has_error = has_error || e;
}
// if _this.image_preprocessor() returns `false`,
// will treat as bad file.
if (contents === undefined)
contents = XMLHttp.buffer;
}
var has_error = !contents || !(contents.length >= _this.MIN_LENGTH)
|| (XMLHttp.status / 100 | 0) !== 2, verified_image;
// console.trace([ image_url, XMLHttp.responseURL ]);
if (!image_data.is_bad
// image_data.is_bad may be set by _this.image_preprocessor()
&& typeof _this.is_limited_image_url === 'function') {
// 處理特殊圖片: 檢查是否下載到 padding 用的 404 檔案。
image_data.is_bad = _this.is_limited_image_url(
XMLHttp.responseURL, image_data);
if (!image_data.is_bad)
delete image_data.is_bad;
else if (image_data.is_bad === true)
image_data.is_bad = 'is limited image';
}
if (!has_error) {
image_data.file_length.push(contents.length);
library_namespace.debug({
// gettext_config:{"id":"completed-image-testing-$1"}
T : [ '測試圖片是否完整:%1', image_data.file ]
}, 2, 'get_image');
var file_type = library_namespace.file_type(contents);
verified_image = file_type && !file_type.damaged;
if (verified_image) {
// console.log(_this.image_types);
if (!file_type.extensions
//
|| !file_type.extensions.some(function(extension) {
return extension in _this.image_types;
})) {
verified_image = false;
library_namespace
.warn({
T : [
// gettext_config:{"id":"unable-to-process-image-file-of-type-$2-$1"}
file_type.type ? '無法處理類型為 %2 之圖片檔:%1'
// gettext_config:{"id":"unable-to-determine-the-type-of-image-file-$1"}
: '無法判別圖片檔之類型:%1', image_data.file,
file_type.type ]
});
}
var original_extension
//
= image_data.file.match(/[^.]*$/)[0].toLowerCase();
if (file_type.extensions ?
//
!image_data.file.endsWith('.' + file_type.extension)
// accept '.jpeg' as alias of '.jpg'
&& !file_type.extensions.includes(original_extension)
// 無法判別圖片檔類型時,若原副檔名為圖片檔案類別則採用之。
: !(original_extension in _this.image_types)) {
// 依照所驗證的檔案格式改變副檔名。
image_data.file = image_data.file.replace(/[^.]+$/,
// e.g. .png
file_type.extension
// 若是沒有辦法判別延伸檔名,那麼就採用預設的圖片延伸檔名。
|| _this.default_image_extension);
}
}
}
// verified_image===true 則必然(!!has_error===false)
// has_error表示下載過程發生錯誤光是檔案損毀不會被當作has_error!
// has_error則必然(!!verified_image===false)
if (false) {
console.log([ _this.skip_error, _this.MAX_ERROR_RETRY,
//
_this.MIN_LENGTH, has_error, _this.skip_error
//
&& image_data.error_count === _this.MAX_ERROR_RETRY ]);
// 出錯次數
library_namespace.log({
// gettext_config:{"id":"number-of-errors-$1"}
T : [ '{{PLURAL:%1|%1}} 次錯誤', image_data.error_count ]
});
}
if (verified_image || image_data.is_bad || _this.skip_error
// 有出問題的話最起碼都需retry足夠次數。
&& image_data.error_count === _this.MAX_ERROR_RETRY
//
|| _this.allow_EOI_error
//
&& image_data.file_length.length > _this.MAX_EOI_ERROR) {
// console.log(image_data.file_length);
if (verified_image || image_data.is_bad || _this.skip_error
// skip error 的話,不管有沒有下載/獲取過檔案(包括404圖像不存在),依然 pass。
// && image_data.file_length.length === 0
//
|| image_data.file_length.cardinal_1()
// ↑ 若是每次都得到相同的檔案長度,那就當作來源檔案本來就有問題。
&& (_this.skip_error || _this.allow_EOI_error
//
&& image_data.file_length.length > _this.MAX_EOI_ERROR)) {
// 圖片下載過程結束,不再嘗試下載圖片:要不是過關,要不就是錯誤太多次了。
var bad_file_path = _this.EOI_error_path(image_data.file,
XMLHttp);
if (has_error || image_data.is_bad
|| verified_image === false) {
image_data.file = bad_file_path;
image_data.has_error = true;
if (_this.preserve_bad_image) {
library_namespace.warn([ {
T : has_error ? contents
// gettext_config:{"id":"force-non-image-files-to-be-saved-as-images"}
? '強制將非圖片檔儲存為圖片。'
// gettext_config:{"id":"force-empty-content-to-be-saved-as-an-image"}
: '強制將空內容儲存為圖片。'
// assert: (!!verified_image===false)
// 圖檔損壞: e.g., Do not has EOI
// gettext_config:{"id":"force-storage-of-damaged-image"}
: '強制儲存損壞的圖片。'
}, XMLHttp.status
// 狀態碼正常就不顯示。
&& (XMLHttp.status / 100 | 0) !== 2 ? {
// gettext_config:{"id":"http-status-code-$1"}
T : [ 'HTTP status code %1.', XMLHttp.status ]
} : '',
// 顯示 crawler 程式指定的錯誤。
image_data.is_bad ? {
// gettext_config:{"id":"error-$1"}
T : [ 'Error: %1.', image_data.is_bad ]
} : '', contents ? {
// gettext_config:{"id":"file-size-$1"}
T : [ 'File size: %1.',
//
CeL.to_KiB(contents.length) ]
} : '',
//
': ' + image_data.file + '\n← ' + image_url ]);
}
if (!contents
// 404之類就算有內容也不過是錯誤訊息頁面。
|| (XMLHttp.status / 100 | 0) === 4) {
contents = '';
}
} else {
// pass, 過關了。
if (node_fs.existsSync(bad_file_path)) {
library_namespace.info({
// gettext_config:{"id":"delete-corrupted-old-image-file-$1"}
T : [ '刪除損壞的舊圖片檔:%1', bad_file_path ]
});
library_namespace.fs_remove(bad_file_path);
}
if (bad_image_archived) {
// 登記壓縮檔內可以刪除的損壞圖檔。
images_archive.to_remove.push(bad_image_archived);
}
}
var old_file_status, old_archived_file =
// image_data.has_error?bad_image_archived:image_archived
image_archived || bad_image_archived;
try {
old_file_status = node_fs.statSync(image_data.file);
} catch (e) {
// old/bad file not exist
}
if (old_archived_file && (!old_file_status
//
|| old_archived_file.size > old_file_status.size)) {
// 壓縮檔內的圖像質量更好的情況,那就採用壓縮檔的。
if (old_file_status
&& old_archived_file.size < contents.length) {
library_namespace.warn({
T : [ _this.archive_images
// gettext_config:{"id":"the-quality-of-the-image-in-the-archive-is-better-than-in-the-directory-but-will-be-overwritten-after-downloading-$1"}
? '壓縮檔內的圖片品質比目錄中的更好,但在下載完後將可能在壓縮時被覆蓋:%1'
// gettext_config:{"id":"the-quality-of-the-image-in-the-archive-is-better-than-in-the-directory-$1"}
: '壓縮檔內的圖片品質比目錄中的更好:%1',
//
old_archived_file.path ]
});
}
old_file_status = old_archived_file;
}
if (!old_file_status || _this.overwrite_old_file
// 得到更大的檔案,寫入更大的檔案。
&& !(old_file_status.size >= contents.length)) {
if (_this.image_post_processor) {
// 圖片後處理程序 image post-processing
contents = _this.image_post_processor(contents,
image_data
// , images_archive
)
|| contents;
}
if (!image_data.has_error || _this.preserve_bad_image) {
library_namespace.debug({
// gettext_config:{"id":"save-image-data-to-your-hard-drive-$1"}
T : [ '保存圖片數據到硬碟上:%1', image_data.file ]
}, 1, 'get_image');
// TODO: 檢查舊的檔案是不是文字檔。例如有沒有包含 HTML 標籤。
try {
node_fs
.writeFileSync(image_data.file,
contents);
} catch (e) {
library_namespace.error(e);
// gettext_config:{"id":"unable-to-write-to-image-file-$1"}
var message = [ gettext('無法寫入圖片檔案 [%1]。',
image_data.file) ];
if (e.code === 'ENOENT') {
message.push(gettext(
// TODO: show chapter_directory 當前作品章節目錄:
// gettext_config:{"id":"it-may-be-because-the-download-directory-of-the-work-has-changed-and-the-cache-data-points-to-the-old-location-that-does-not-exist"}
'可能因為作品下載目錄改變了,而快取資料指向不存在的舊位置。'));
} else {
message.push(gettext(
// gettext_config:{"id":"it-may-be-because-the-work-information-cache-is-different-from-the-structure-of-the-work-chapter-on-the-current-website"}
'可能因為作品資訊快取與當前網站上之作品章節結構不同。'));
}
message.push(gettext(
// https://github.com/kanasimi/work_crawler/issues/278
// gettext_config:{"id":"if-you-have-downloaded-this-work-before-please-save-the-original-work-catalog-or-rename-the-work-cache-file-(the-work-id.json-under-the-work-directory)-and-try-the-new-download"}
'若您之前曾經下載過本作品的話,請封存原有作品目錄,或將作品資訊快取檔(作品目錄下的 作品id.json改名之後嘗試全新下載。'
//
));
_this.onerror(message.join('\n'), image_data);
if (typeof callback === 'function') {
callback(image_data,
'image_file_write_error');
}
return Work_crawler.THROWED;
}
}
} else if (old_file_status
&& old_file_status.size > contents.length) {
library_namespace.log({
T : [
// gettext_config:{"id":"there-is-a-large-old-file-($2)-that-will-not-be-overwritten-$1"}
'存在較大的舊檔 (%2),將不覆蓋:%1',
image_data.file,
old_file_status.size + '>'
+ contents.length ]
});
}
image_data.done = true;
if (typeof callback === 'function')
callback(image_data/* , 'OK' */);
return;
}
}
// 有錯誤。下載圖像錯誤時報錯。
var message;
if (verified_image === false) {
// 圖檔損壞: e.g., Do not has EOI
message = [ {
// gettext_config:{"id":"image-damaged"}
T : '圖檔損壞:'
} ];
} else {
// 圖檔沒資格驗證。
message = [ {
// gettext_config:{"id":"failed-to-get-image"}
T : '無法取得圖片。'
}, XMLHttp.status ? {
// gettext_config:{"id":"http-status-code-$1"}
T : [ 'HTTP status code %1.', XMLHttp.status ]
} : '', {
// gettext_config:{"id":"image-without-content"}
T : !contents ? '圖片無內容:' : [
//
contents.length < _this.MIN_LENGTH
// gettext_config:{"id":"$1-bytes-too-small"}
? '檔案過小,僅 %1 {{PLURAL:%1|位元組}}'
// gettext_config:{"id":"$1-bytes"}
: '檔案僅 %1 {{PLURAL:%1|位元組}}', contents.length ]
} ];
}
message.push(image_url + '\n→ ' + image_data.file);
library_namespace.warn(message);
// Release memory. 釋放被占用的記憶體。
message = null;
if (image_data.error_count === _this.MAX_ERROR_RETRY) {
image_data.has_error = true;
// throw new Error(_this.id + ': ' +
// gettext('MESSAGE_NEED_RE_DOWNLOAD'));
library_namespace.log(_this.id + ': '
// gettext_config:{"id":"message_need_re_download"}
+ gettext('MESSAGE_NEED_RE_DOWNLOAD'));
// console.log('error count: ' + image_data.error_count);
if (contents && contents.length > 10
//
&& contents.length < _this.MIN_LENGTH
// 檔案有驗證過,只是太小時,應該不是 false。
&& verified_image !== false
// 就算圖像是完整的只是比較小HTTP status code 也應該是 2xx。
&& (XMLHttp.status / 100 | 0) === 2) {
library_namespace.warn([ {
// gettext_config:{"id":"perhaps-the-image-is-complete-just-too-small-and-not-up-to-standard-such-as-an-almost-blank-image"}
T : '或許圖片是完整的,只是過小而未達標,例如幾乎為空白之圖片。'
}, {
// gettext_config:{"id":"work_crawler-skip-image-error-prompt"}
T : [ 'work_crawler-skip-image-error-prompt',
//
contents.length,
//
JSON.stringify(_this.EOI_error_postfix) ]
} ]);
} else if (image_data.file_length.length > 1
&& !image_data.file_length.cardinal_1()) {
library_namespace.warn([ {
// gettext_config:{"id":"the-downloaded-image-is-different-in-size-$1"}
T : [ '下載所得的圖片大小不同:%1。', image_data.file_length ]
}, {
// gettext_config:{"id":"if-it-is-not-because-the-website-cuts-off-the-connection-early-then-you-may-need-to-increase-the-time-limit-to-provide-enough-time-to-download-the-image"}
T : '若非因網站提早截斷連線,那麼您或許需要增長時限來提供足夠的時間下載圖片?'
} ]);
// TODO: 提供續傳功能。
// e.g., for 9mdm.js→dagu.js 魔剑王 第59话 4392-59-011.jpg
} else if (!_this.skip_error) {
library_namespace.info([ {
// gettext_config:{"id":"if-the-error-persists-you-can-set-skip_error=true-to-ignore-the-image-error"}
T : '若錯誤持續發生,您可以設定 skip_error=true 來忽略圖片錯誤。'
}, {
// gettext_config:{"id":"you-must-set-the-skip_error-or-allow_eoi_error-option-to-store-corrupted-files"}
T : '您必須設定 skip_error 或 allow_EOI_error 選項,才會儲存損壞的檔案。'
}, {
// gettext_config:{"id":"if-you-need-to-re-download-the-section-that-failed-to-download-before-turn-on-the-recheck-option"}
T : '若您需要重新下載之前下載失敗的章節,請開啟 recheck 選項。'
} ]);
}
// gettext_config:{"id":"failed-to-download-image"}
_this.onerror(gettext('圖片下載錯誤'), image_data);
// image_data.done = false;
if (typeof callback === 'function')
callback(image_data, 'image_download_error');
return Work_crawler.THROWED;
// 網頁介面不可使用process.exit(),會造成白屏
// process.exit(1);
}
image_data.error_count = (image_data.error_count | 0) + 1;
library_namespace.log([ 'get_image: ', {
// gettext_config:{"id":"retry-$1-$2"}
T : [ 'Retry %1/%2',
//
image_data.error_count, _this.MAX_ERROR_RETRY ]
}, '...' ]);
var get_image_again = function() {
_this.get_image(image_data, callback, images_archive);
}
if (image_data.time_interval > 0) {
library_namespace.log_temporary([ 'get_image: ', {
// gettext_config:{"id":"waiting-for-$2-and-retake-the-image-$1"}
T : [ '等待 %2 之後再重新取得圖片:%1', image_data.url,
//
library_namespace.age_of(0, image_data.time_interval, {
digits : 1
}) ]
} ]);
setTimeout(get_image_again, image_data.time_interval);
} else
get_image_again();
}, null, Object.assign({
/**
* 最多平行下載/獲取檔案(圖片)的數量。
*
* <code>
incase "MaxListenersExceededWarning: Possible EventEmitter memory leak detected. 11 connect listeners added. Use emitter.setMaxListeners() to increase limit"
</code>
*/
max_listeners : 300,
fetch_type : 'image'
}, this.get_URL_options, image_data.get_URL_options), 'buffer');
}
// --------------------------------------------------------------------------------------------
// export 導出.
// @instance
Object.assign(Work_crawler.prototype, {
// 可接受的圖片類別(延伸檔名)。以 "|" 字元作分隔,如 "webp|jpg|png"。未設定將不作檢查。輸入 "images"
// 表示接受所有圖片。
// 若下載的圖片不包含在指定類型中,則會視為錯誤。本工具只能下載特定幾種圖片類型。本選項僅供檢查圖片,非用來挑選想下載的圖片類型。
// {Array|String}可以接受的圖片類別/圖片延伸檔名/副檔名/檔案類別 acceptable file extensions。
// acceptable_types : 'images',
// acceptable_types : 'png',
// acceptable_types : 'webp|png',
// acceptable_types : ['webp', 'png'],
// 當圖片不存在 EOI (end of image) 標記,或是被偵測出非圖片時,依舊強制儲存檔案。
// allow image without EOI (end of image) mark. default:false
// allow_EOI_error : true,
// 圖片檔案下載失敗處理方式:忽略/跳過圖片錯誤。當404圖片不存在、檔案過小或是被偵測出非圖片(如不具有EOI)時依舊強制儲存檔案。default:false
// skip_error : true,
//
// 若已經存在壞掉的圖片就不再嘗試下載圖片。default:false
// skip_existed_bad_file : true,
//
// 循序逐個、一個個下載圖片。僅對漫畫有用,對小說無用。小說章節皆為逐個下載。 Download images one by one.
// default: 同時下載本章節中所有圖片。 Download ALL images at the same time.
// 若設成{Natural}大於零的數字(ms)或{String}時間長度,那會當成下載每張圖片之時間間隔 time_interval。
// cf. .chapter_time_interval
// one_by_one : true,
//
// e.g., '2-1.jpg' → '2-1 bad.jpg'
EOI_error_postfix : ' bad',
// 加上有錯誤檔案之註記。
EOI_error_path : EOI_error_path,
image_path_to_url : image_path_to_url,
get_image : get_image
});
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

View File

@@ -0,0 +1,206 @@
/**
* @name WWW work crawler sub-functions
*
* @fileoverview WWW work crawler functions: part of search
*
* @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.search',
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, crawler_namespace = Work_crawler.crawler_namespace;
var gettext = library_namespace.locale.gettext;
// --------------------------------------------------------------------------------------------
// @see luoxia.js, dmzj.js
function parse_search_result_token(id_list, id_data, token_parser, token) {
var matched = token.match(/<a\s([^<>]+)>([\s\S]+?)<\/a>/i);
if (library_namespace.is_RegExp(token_parser)) {
matched = token.match(token_parser);
} else {
// matched: [ link, attributes, inner HTML ]
matched = token.match(/<a\s([^<>]+)>([\s\S]+?)<\/a>/i);
}
if (!matched)
return;
var id = matched[1]
// dmzj.js: title=""href="" 中間沒有空格。
.match(/href=["'][^"'<>]+?\/([a-z\d\-_]+)(?:\/|\.html)?["']/i);
if (!id)
return;
id = id[1];
if (false && !isNaN(id)) {
id = +id;
}
id_list.push(id);
var title = matched[1].match(/title=["']([^"'<>]+)["']/);
id_data.push(crawler_namespace.get_label(title && title[1]
|| matched[2]));
}
// only for .parse_search_result() !!
function extract_work_id_from_search_result_link(PATTERN_item_token, html,
token_parser) {
// console.log(html);
var matched,
// {Array}id_list = [ id, id, ... ]
id_list = [],
// {Array}id_data = [ title, title, ... ]
id_data = [],
//
search_result_parser = typeof token_parser === 'function'
//
&& function(token) {
// function parser(token, id_list, id_data){console.log(token);}
var result = token_parser.call(this, token, id_list, id_data);
if (Array.isArray(result) && result.length === 2) {
id_list.push(result[0]);
id_data.push(result[1]);
}
};
// PATTERN_item_token 會分離出每個作品的欄位。
if (library_namespace.is_RegExp(PATTERN_item_token)) {
// assert: PATTERN_item_token.global === true
// matched: [ , HTML token to check ]
while (matched = PATTERN_item_token.exec(html)) {
if (search_result_parser) {
search_result_parser(matched[1]);
} else {
parse_search_result_token(id_list, id_data, token_parser,
matched[1]);
}
}
} else if (Array.isArray(PATTERN_item_token)) {
html.each_between(PATTERN_item_token[0], PATTERN_item_token[1],
search_result_parser
|| parse_search_result_token.bind(null, id_list,
id_data, token_parser));
} else {
throw new TypeError('extract_work_id_from_search_result_link: '
// gettext_config:{"id":"invalid-token-pattern-{$1}-$2"}
+ gettext('Invalid token pattern: {%1} %2',
typeof PATTERN_item_token, JSON
.stringify(PATTERN_item_token)));
}
// console.log([ id_list, id_data ]);
// throw 'extract_work_id_from_search_result_link';
return [ id_list, id_data ];
}
// CeL.work_crawler.extract_work_id_from_search_result_link()
Work_crawler.extract_work_id_from_search_result_link = extract_work_id_from_search_result_link;
// --------------------------------------------------------------------------------------------
var PATTERN_url_for_baidu = /([\d_]+)(?:\.html|\/(?:index\.html)?)?$/;
if (library_namespace.is_debug()) {
[ 'http://www.host/0/123/', 'http://www.host/123/index.html',
'http://www.host/123.html' ].forEach(function(url) {
console.assert('123' === 'http://www.host/123/'
.match(PATTERN_url_for_baidu)[1]);
});
}
crawler_namespace.parse_search_result_set = {
// baidu cse
baidu : function(html, get_label) {
// console.log(html);
var id_data = [],
// {Array}id_list = [id,id,...]
id_list = [], get_next_between = html.find_between(
' cpos="title" href="', '</a>'), text;
while ((text = get_next_between()) !== undefined) {
// console.log(text);
// 從URL網址中解析出作品id。
var matched = text.between(null, '"').match(
PATTERN_url_for_baidu);
// console.log(matched);
if (!matched)
continue;
id_list.push(matched[1]);
// 從URL網址中解析出作品title。
matched = text.match(/ title="([^"]+)"/);
if (matched) {
matched = matched[1];
} else {
// e.g., omanhua.js: <em>择</em><em>天</em><em>记</em>
matched = text.between('<em>', {
tail : '</em>'
});
}
// console.log(matched);
if (matched && (matched = get_label(matched))
// 只取第一個符合的。
// 避免如 http://host/123/, http://host/123/456.htm
&& !id_data.includes(matched)) {
id_data.push(matched);
} else {
id_list.pop();
}
}
// console.log([ id_list, id_data ]);
return [ id_list, id_data ];
}
};
// --------------------------------------------------------------------------------------------
// export 導出.
// @instance
Object.assign(Work_crawler.prototype, {
search_result_file_name : 'search.json',
cache_title_to_id : true,
get_search_result_file : function() {
var search_result_file = this.main_directory
+ this.search_result_file_name;
return search_result_file;
},
get_search_result : function() {
var search_result_file = this.get_search_result_file(),
// search cache
// 檢查看看之前是否有獲取過。
search_result = library_namespace.get_JSON(search_result_file);
return search_result;
}
});
// 不設定(hook)本 module 之 namespace僅執行 module code。
return library_namespace.env.not_to_extend_keyword;
}

View File

@@ -0,0 +1,233 @@
/**
* @name CeL module for downloading AlphaPolis user novels / comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 アルファポリス - 電網浮遊都市 - 小説 / 漫画 的工具。
*
* <code>
CeL.AlphaPolis({
// configuration
site : '' || CeL.get_script_name()
}).start(work_id);
</code>
*
* @since 2020/7/18 5:31:55 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/novel.ja-JP/AlphaPolis.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.ja-JP/AlphaPolis_user_manga.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.ja-JP/AlphaPolis_official_manga.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.AlphaPolis',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 當網站不允許太過頻繁的訪問/access時可以設定下載之前的等待時間(ms)。
// 模仿實際人工請求。
// chapter_time_interval : '5s',
base_URL : 'https://www.alphapolis.co.jp/',
// @deprecated: novel
// till 20170619, use POST. 20170620 AlphaPolis 改版, use UTF-8.
search_URL_2016_to_20170619 : function(work_title) {
return [ 'top/search/', {
// 2: 小説
'data[tab]' : 2,
'data[refer]' : work_title
} ];
},
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
return 'search?category='
+ this.work_type.split('/').reverse().join('_') + '&query='
+ encodeURIComponent(work_title.replace(/\s+\d+$/, ''));
},
// for AlphaPolis_user_manga.js , AlphaPolis_official_manga.js
parse_search_result : function(html, get_label) {
// console.log(html);
var _this = this, id_data = [],
// {Array}id_list = [id,id,...]
id_list = [];
html.each_between(' class="title">', '</a>', function(text) {
// console.log(text);
var id = text.between(' href="/' + _this.work_type + '/', '"');
if (id) {
id_list.push(id.replace(/\//, '-'));
id_data.push(get_label(text.between('>')));
}
});
// console.log([ id_list, id_data ]);
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return this.work_type + '/' + work_id.replace('-', '/');
},
// for AlphaPolis.js , AlphaPolis_user_manga.js
parse_work_data : function(html, get_label, extract_work_data) {
var work_data = {
// 必要屬性:須配合網站平台更改。
// 2019/8/16 19:0 改版。
title : get_label(html.between('<h1 class="title">', '</h1>')
|| html.between('<h2 class="title">', '</h1>')),
// 選擇性屬性:須配合網站平台更改。
// e.g., 连载中, 連載中
status : [],
// get the first <div class="author">...</div>
author : get_label(html.between('<div class="author">', '</a>')),
last_update : get_label(html.between('<th>更新日時</th>', '</td>')),
description : get_label(
html.between('<div class="abstract">', '</div>'))
.replace(/\n{2}/g, '\n'),
ranking : get_label(html.between('<div class="ranking">',
// also category
'</div>')).replace(/\t/g, '').replace(/\n{3}/g, '\0').replace(
/\n/g, ' ').split('\0'),
// site_name : 'アルファポリス'
language : html.between('data-lang="', '"') || 'ja-JP'
}, PATTERN, matched;
if (work_data.title === 'Not Found' && !work_data.author) {
// 對於已經失效的作品,直接中斷下載。
throw work_data.title;
}
if (work_data.site_name) {
// "アルファポリス - 電網浮遊都市 - " → "アルファポリス"
work_data.site_name = work_data.site_name.replace(/ +- .+/, '');
}
// 2019/1 才發現改 pattern 了。
PATTERN = /<span class="tag">([\s\S]+?)<\/span>/g;
while (matched = PATTERN.exec(html.between(
'<div class="content-tags">', '</div>'))) {
work_data.status.push(get_label(matched[1]));
}
html.between('<div class="content-statuses">', '</div>')
// additional tags
.each_between(' class="content-status', '<',
//
function(text) {
work_data.status.push(get_label(text.between('>')));
});
work_data.status = work_data.status.unique();
// <h2>作品の情報</h2>
extract_work_data(work_data, html,
/<th>([\s\S]+?)<\/th>[\s\n]*<td[^<>]*>([\s\S]+?)<\/td>/g);
extract_work_data(work_data, html);
if (work_data.image
// ignore site default image
&& work_data.image.endsWith('\/ogp.png')) {
delete work_data.image;
}
// console.log(work_data);
return work_data;
},
// for AlphaPolis.js , AlphaPolis_user_manga.js
get_chapter_list : function(work_data, html) {
work_data.chapter_list = [];
var _this = this;
html = html.between('<div class="nav">', '<div class="freespace">')
//
.each_between('<a href="/'
//
+ _this.work_type + '/', '</a>', function(text) {
work_data.chapter_list.push({
url : '/' + _this.work_type + '/'
//
+ text.between(null, '"'),
date : text.between('<span class="open-date">', '</span>')
//
.to_Date({
zone : work_data.time_zone
}),
title : text.between(' class="title">',
// '<span class="title"><span
// class="bookmark-dummy"></span>',
// '</span>'
'<span class="open-date">')
});
});
},
// for AlphaPolis_user_manga.js , AlphaPolis_official_manga.js
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// console.log(html);
var chapter_data = work_data.chapter_list[chapter_NO - 1];
Object.assign(chapter_data, {
// 設定必要的屬性。
title : get_label(html.between('<h2>', '</h2>')),
image_list : []
});
html.each_between('_pages.push("', '"', function(url) {
if (url.includes('://'))
chapter_data.image_list.push(url);
});
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_AlphaPolis_crawler(configuration, callback, initializer) {
// library_namespace.set_debug(9);
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
if (configuration.need_create_ebook) {
library_namespace.run([ 'application.storage.EPUB'
// CeL.detect_HTML_language()
, 'application.locale' ]);
}
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
// for 年齢確認 eternityConfirm()
crawler.setup_value('cookie', [ 'confirm='
+ Math.floor(Date.now() / 1000)
// location.hostname
+ ';domain=' + crawler.base_URL.match(/\/\/([^\/]+)/)[1]
+ ';path=/;' ]);
return crawler;
}
return new_AlphaPolis_crawler;
}

View File

@@ -0,0 +1,375 @@
/**
* @name CeL module for downloading PTCMS novels.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見小說管理系統: PT小说聚合程序 (PTCMS系统) 各個版本的工具。
*
* <code>
CeL.PTCMS(configuration).start(work_id);
</code>
*
* TODO: 去掉前後網站廣告。
*
* @see https://www.ptcms.com/
* @see http://down.chinaz.com/test/201210/2252_1.htm 杰奇小说连载系统 杰奇原创文学系统,
* https://zhidao.baidu.com/question/518711125119801445.html 奇文网络小说管理系统
* 终点小说网站管理系统 露天中文小说网站管理系统 https://zhidao.baidu.com/question/474414436.html
* https://www.ptcms.com/ 关关采集器
*
* @see https://github.com/LZ0211/Wedge/tree/master/lib/Sites/plugins
* https://github.com/lufengfan/NovelDownloader
* https://github.com/unclezs/NovelHarvester
* @see http://www.sodu.cc/default.html
* https://kknews.cc/zh-tw/culture/oqyx5.html https://tw.hjwzw.com/
* @see http://www.76wx.com/ http://www.xssk.net/
*
* @since 2017/6/19 21:15:40 模組化。
*/
// More examples:
// @see https://github.com/kanasimi/work_crawler/blob/master/81xsw.js
// @see https://github.com/kanasimi/work_crawler/blob/master/23us.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.PTCMS',
require : 'application.net.work_crawler.'
//
+ '|application.storage.EPUB.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
function is_server_error(result) {
// TODO: 81xsw 有時會 403需要重新再擷取一次。
return result in {
// 88dus 有時會 502需要重新再擷取一次。
'502 Bad Gateway' : true,
// 630book 有時會 503需要重新再擷取一次。
'503 Service Unavailable' : true,
// 630book 有時會 "500 - 内部服务器错误。"
'服务器错误' : true
};
}
var default_configuration = {
// auto_create_ebook, automatic create ebook
// MUST includes CeL.application.locale!
need_create_ebook : true,
// recheck:從頭檢測所有作品之所有章節。
// 'changed': 若是已變更,例如有新的章節,則重新下載/檢查所有章節內容。
recheck : 'changed',
// base_URL : 'http://www.*.com/',
// charset : 'gbk',
// 解析 作品名稱 → 作品id get_work()
// search_URL : '',
// for 笔趣阁
parse_search_result_biquge : function(html, get_label) {
// console.log(html);
var matched = html
.match(/og:url" content="[^<>"]+?\/(?:\d+_)?(\d+)\/"/);
if (matched) {
return [ [ +matched[1] ],
[ get_label(html.between('og:title" content="', '"')) ] ];
}
matched = html.match(/blockcontent">([\s\S]+)<\/div>/);
if (matched) {
/**
* <code>
// xbiquge.cc.js:
<div class="blocktitle">出现错误!</div><div class="blockcontent"><div style="padding: 10px"><br /> 错误原因:对不起,两次搜索的间隔时间不得少于 30 秒<br /><br /> 请 <a href="javascript:history.back(1)">返 回</a> 并修正<br /><br /></div><div style="width: 100%; text-align: right; line-height: 200%; padding-right: 10px;">[<a href="javascript:window.close()">关闭本窗口</a>]</div></div>
</code>
*/
matched = get_label(matched[1]).replace(/\n[\s\S]*/, '');
library_namespace.error(matched);
}
// console.trace(html);
var id_list = [], id_data = [];
html.each_between('<li>', '</li>', function(text) {
matched = text.match(
/**
* <code>
// biquge.js:
<span class="s2"><a href="https://www.xs.la/211_211278/" target="_blank">
万古剑神</a>
</span>
// xbiquge.js:
<span class="s2"><a href="http://www.xbiquge.cc/book/24276/">元尊</a></span>
// xbiquke.js
<span class="s1">1</span>
<span class="s2">
<a href="/29_29775/" target="_blank">
我真不是邪神走狗
</a>
</span>
<span class="s4">
<a href="/author/29775/">
万劫火
</a>
</span>
<span class="s3">
<a style="color: Red;" href="/29_29775/22709468.html" target="_blank" title="番外·童年(一)">
番外·童年(一)</a>
</span>
<span class="s6">2021-11-26 02:30:39</span>
</code>
*/
/<a href="[^<>"]*?\/(?:\d+_)?(\d+)\/"[^<>]*>([\s\S]+?)<\/a>/);
// console.log([ text, matched ]);
if (matched) {
id_list.push(+matched[1]);
id_data.push(get_label(matched[2]));
}
});
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
// work_URL : function(work_id) {
// /** @see this.work_URL in CeL.application.net.work_crawler */ },
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
// 由 meta data 取得作品資訊。
var work_data = {
// 必要屬性:須配合網站平台更改。
title : html.between('og:novel:title" content="', '"')
// e.g., 88dushu.js
|| html.between('og:title" content="', '"')
// 通常與og:title相同
|| html.between('og:novel:book_name" content="', '"'),
// 選擇性屬性:須配合網站平台更改。
author : html.between('og:novel:author" content="', '"'),
// e.g., 连载[中], [已]完成
status : html.between('og:novel:status" content="', '"'),
category : html.between('og:novel:category" content="', '"'),
// https://www.booktxt.net/: '<div id="fmimg">'
image : html.between('<div id="fmimg">', '</div>').between(
'<img src="', '"')
// general
|| html.between('og:image" content="', '"'),
last_update :
//
html.between('og:novel:update_time" content="', '"')
// e.g., 630book
|| html.between('og:novel:update_time" content=\'', "'"),
latest_chapter : html.between(
'og:novel:latest_chapter_name" content="', '"'),
description : get_label(
//
html.between('og:description" content="', '"')
// e.g., 630book
|| html.between('<div id="intro">', '</div>'))
// 偶爾會有沒填上描述的書籍。
|| '',
language : 'cmn-Hans-CN',
site_name : get_label(
//
html.between('<div class="logo">', '</div>')
//
|| html.between('<div class="header_logo">', '</div>')
// e.g., 630book
|| html.between('<strong class="logo">', '</strong>'))
};
// 由 meta data 取得作品資訊。
extract_work_data(work_data, html);
if (is_server_error(work_data.title)) {
return this.REGET_PAGE;
}
if (this.extract_work_data) {
// e.g., xbiquke.js
this.extract_work_data(work_data, html, get_label,
extract_work_data);
}
if (/^\d{1,2}-\d{1,2}$/.test(work_data.last_update)) {
// e.g., 07-01 → 2017-07-01
work_data.last_update = (new Date).getFullYear() + '-'
+ work_data.last_update;
}
if (work_data.site_name.includes('?$')) {
// e.g., 88dushu.js
work_data.site_name = html.between("AddFavorite('", "'");
}
// console.log(work_data);
return work_data;
},
// 取得包含章節列表的文字範圍。
// get_chapter_list_contents : function(html) {return html.between();},
get_chapter_list : function(work_data, html, get_label) {
// determine base directory of work
work_data.base_url = work_data.url.endsWith('/') ? work_data.url
: work_data.url.replace(/\.[^.]+$/, '/');
if (work_data.base_url.startsWith(this.base_URL)) {
work_data.base_url = work_data.base_url
.slice(this.base_URL.length - 1);
}
if (this.get_chapter_list_contents) {
html = this.get_chapter_list_contents(html);
}
// console.log(html);
work_data.chapter_list = [];
var part_title, matched,
// 章節以及篇章連結的模式。
// [ all, tag name, attributes, 連結內容 HTML ]
PATTERN_chapter = /<(li|dd|dt)([^<>]*)>([\s\S]*?)<\/\1>/g;
while (matched = PATTERN_chapter.exec(html)) {
if (false) {
delete matched.input;
console.log(matched);
}
if (matched[1] === 'dt' ||
// e.g., 88dushu.js
matched[1] === 'li' && matched[2].includes('class="fj"')) {
part_title = get_label(matched[3]);
if (part_title.includes('最新章节') && part_title.length > 20) {
// e.g., 《...》最新章节(提示:已启用缓存技术,最新章节可能会延时显示,登录书架即可实时查看。)
// e.g., ...最新章节列表 (本页已经缓存,请加入书架查看...最新章节)
part_title = 'pass';
} else if (part_title.includes('正文')) {
// e.g., 《...》正文卷, 《...》正文
part_title = '';
}
// console.log(part_title);
} else if (part_title !== 'pass'
// 取得連結內容。
&& (matched = matched[3].between('<a ', '</a>'))) {
var chapter_data = {
// 從href取得章節的網址。
url : matched.between('href="', '"')
// xbiquge.js: 交錯使用 "", ''
|| matched.between("href='", "'")
// booktxt.js: 交錯使用 "", ''
|| matched.between('href ="', '"'),
part_title : part_title,
// 從title/顯示的文字取得章節的標題。
title : matched.between('title="', '"')
|| get_label(matched.between('>'))
};
work_data.chapter_list.push(chapter_data);
}
}
// console.log(work_data.chapter_list);
},
// 取得每一個章節的內容與各個影像資料。 get_chapter_data()
chapter_URL : function(work_data, chapter_NO) {
var url = work_data.chapter_list[chapter_NO - 1].url;
// console.trace(url);
url = url.startsWith('/') || url.includes('://') ? url
: work_data.base_url + url;
// console.trace(url);
return url;
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
if (!html && this.skip_error === true) {
// Skip empty chapter
return;
}
// 在取得小說章節內容的時候,若發現有章節被目錄漏掉,則將之補上。
this.check_next_chapter(work_data, chapter_NO, html,
this.PATTERN_next_chapter);
var chapter_data = work_data.chapter_list[chapter_NO - 1],
//
sub_title = get_label(html.between('<h1>', '</h1>'))
// || get_label(html.between('<H1>', '</H1>'))
// || chapter_data.title
, text = (html
// general: <div id="content">
// xbiquge.js: <div id="content" name="content">
.between('<div id="content"', '</div>').between('>')
// 去除掉廣告。
// e.g., 88dushu.js
|| html.between('<div class="yd_text2">', '</div>')).replace(
/<script[^<>]*>[^<>]*<\/script>/g, ''),
//
KEY_interval_cache = 'original_chapter_time_interval';
if (is_server_error(sub_title) && text.length < 2000) {
this[KEY_interval_cache] = this.chapter_time_interval;
// 當網站不允許太過頻繁的訪問/access時可以設定下載之前的等待時間(ms)。
this.chapter_time_interval = 10 * 1000;
return this.REGET_PAGE;
}
if (KEY_interval_cache in this) {
// recover time interval
if (this[KEY_interval_cache] > 0) {
this.chapter_time_interval = this[KEY_interval_cache];
} else {
delete this.chapter_time_interval;
}
delete this[KEY_interval_cache];
}
if (this.remove_ads) {
text = this.remove_ads(text);
}
// console.log(text);
this.add_ebook_chapter(work_data, chapter_NO, {
title : chapter_data.part_title,
sub_title : sub_title,
text : text
});
}
};
// --------------------------------------------------------------------------------------------
function new_PTCMS_novels_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
if (configuration.parse_search_result === 'biquge') {
configuration.parse_search_result = configuration.parse_search_result_biquge;
}
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
return new_PTCMS_novels_crawler;
}

View File

@@ -0,0 +1,634 @@
/**
* @name CeL module for downloading SinMH CMS comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見漫畫管理系統: 圣樱漫画管理系统 (圣樱CMS) MHD模板 PC端 的工具。
*
* <code>
CeL.SinMH(configuration).start(work_id);
</code>
*
* TODO: ONE漫画 https://www.onemanhua.com/ 可能是比 930mh.js 更舊的版本?
*
* @see https://cms.shenl.com/sinmh/
* @see https://www.manhuadui.com/js/common.js "Created by Shen.L on 2016/1/28."
*
* @since 2018/7/26 11:9:53 模組化 MHD模板。<br />
* 2019/2/4 add 930mh.js 使用 CryptoJS採用 DMZJ模板。<br />
* 2019/7/2 50mh.js 使用 CryptoJS採用 DMZJ模板。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/36mh.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/gufengmh.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/930mh.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.SinMH',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 嘗試取得被屏蔽的作品。
// 對於被屏蔽的作品,將會每次都從頭檢查。
try_to_get_blocked_work : true,
// 當有多個分部的時候才重新檢查。
recheck : 'multi_parts_changed',
// allow .jpg without EOI mark.
// allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
// skip_error : true,
// 提取出引數(如 URL中的作品ID 以回傳。
extract_work_id : function(work_information) {
// e.g,
// https://www.36mh.com/manhua/IDOLiSH7ouxiangxingyuanxiangliuxingxuyuan/
return /^[a-z\d]+$/i.test(work_information) && work_information;
},
// 取得伺服器列表。
// use_server_cache : true,
server_URL : 'js/config.js',
parse_server_list : function(html) {
var server_list = [], SinConf;
// console.trace(html);
if (/^\s*var cars/.test(html)) {
// for manhuaniu.js 2021/1/19 改版
html = html.replace('var SinConf', 'SinConf').replace(
/\n}\(\);[\s\S]*/, '}();SinConf.cars=cars;');
} else {
html = html.replace('var ', '').replace(/(}\(\))[\s\S]*/, '$1');
}
// console.trace(html);
eval(html);
function append_path(host) {
return host.endsWith('/') ? host : host + '/';
}
SinConf.resHost.map(function(data) {
server_list.append(data.domain.map(append_path));
});
if (SinConf.cars) {
server_list.append(SinConf.cars.map(append_path));
}
server_list = server_list.unique();
// for manhuaniu.js 2021/1/19 改版
server_list = server_list.filter(function(server) {
return !server.includes('restp.dongqiniqin');
});
server_list.conf = SinConf;
// console.log(SinConf);
// console.log(server_list);
return server_list;
},
// 解析 作品名稱 → 作品id get_work()
// 1. 使用 PC端 網頁取得搜尋所得的作品資料。 (default)
search_URL : 'search/?keywords=',
// 2. 使用API取得搜尋所得的作品資料。 (set search_URL:'API')
search_URL_API : function(work_title) {
// SinConf.apiHost
var apiHost = this.api_base_URL
|| this.base_URL.replace(/\/\/[a-z]+/, '//api');
return [ apiHost + 'comic/search', {
keywords : work_title
} ];
},
parse_search_result : function(html, get_label) {
// console.log(html);
if (html.startsWith('{')) {
// 2. 使用API取得搜尋所得的作品資料。
/**
* e.g.,<code>
{"items":[{"id":3542,"status":1,"commend":0,"is_original":0,"is_vip":0,"name":"军阀霸宠:纯情妖女火辣辣","title":"民国妖闻录","alias":"","original_name":"","letter":"j","slug":"junfabachongchunqingyaonuhuolala","coverUrl":"http://res.gufengmh.com/images/cover/201711/1509877682Xreq-5mrrSsDm82P.jpg","uri":"/manhua/junfabachongchunqingyaonuhuolala/","last_chapter_name":"040纯良少年的堕落","last_chapter_id":235075,"author":"逐浪动漫","author_id":3901,"serialise":1}],"_links":{"self":{"href":"http://api.gufengmh.com/comic/search?page=1"}},"_meta":{"totalCount":1,"pageCount":1,"currentPage":1,"perPage":20},"status":0}
</code>
*/
var id_data = html ? JSON.parse(html).items : [];
// console.log(id_data);
return [ id_data, id_data ];
}
// 1. 使用 PC端 網頁取得搜尋所得的作品資料。
// e.g., 36mh.js
var id_list = [], id_data = [], matched,
// matched: [ all, url, inner (title) ]
PATTERN_search = /<p class="ell"><a href="([^<>"]+)">([^<>]+)/g;
if (matched = html.between('<h4 class="fl">')) {
html = matched;
// matched: [ all, url, inner (title) ]
// PATTERN_search = /<p class="ell"><a
// href="([^<>"]+)">([^<>]+)/g;
} else if (matched = html.between('<div id="update_list">')) {
// 行動版 mobile version
// e.g., <div id="update_list"><div class='UpdateList'><div
// class="itemBox" data-key="10992">
html = matched;
// e.g., <a class="title"
// href="https://m.36mh.com/manhua/dushizhixiuzhenguilai/"
// target="_blank">都市之修真归来</a>
// matched: [ all, url, inner (title) ]
PATTERN_search = /<a class="title" href="([^<>"]+)"[^<>]*>([^<>]+)/g;
} else {
// throw new Error('Unknown site!');
}
while (matched = PATTERN_search.exec(html)) {
// .html: mh1234.js
matched[1] = matched[1].match(/([^\/]+)(?:\/|\.html)$/);
id_list.push(matched[1][1]);
id_data.push(get_label(matched[2]));
}
return [ id_list, id_data ];
},
// e.g., 50mh.js
// id_of_search_result : 'slug',
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
// console.log(work_id);
return 'manhua/' + work_id + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
var work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(html.between('<h1>', '</h1>')),
// 選擇性屬性:須配合網站平台更改。
description : get_label(html.between('intro-all', '</div>')
.between('>')
// 930mh.js
|| html.between('<p class="comic_deCon_d">', '</p>')
// copy from 733mh.js: for mh1234.js
|| html.between(
'<div class="introduction" id="intro1">',
'</div>'))
};
// <div class="book-detail pr fr">
// <ul class="detail-list cf">
// ...
// </ul>
// <a class="intro-act" id="intro-act" href="javascript:;">展開詳情</a>
extract_work_data(work_data, html.between('detail-list', '</ul>'),
// e.g., "<strong>漫画别名:</strong>暂无</span>"
// gufengmh.js:<li><span><strong>漫画类型:</strong>...</span><span><strong>漫画作者:</strong>...</span></li>
/<strong[^<>]*>([^<>]+)<\/strong>([\s\S]+?)<\/(?:li|span)>/g);
// console.log(html.between('detail-list', '</ul>'));
// console.log(work_data);
// 930mh.js
extract_work_data(work_data, html
.between('<ul class="comic_deCon_liT">',
'<p class="comic_deCon_d">')
// <li>时间2019-02-04 <li>最新:<a
// href="/manhua/17884/668443.html">第6话</a></li>
.replace(/<li>/g, '</li><li>'),
// e.g., "<li>类别:<a href="/list/shaonian/">少年</a></li>"
/<li>([^]+)([\s\S]+?)<\/li>/g);
// copy from 733mh.js: for mh1234.js
extract_work_data(work_data, html.between('<div class="info">',
'<div class="info_cover">'),
/<em>([^<>]+?)<\/em>([\s\S]*?)<\/p>/g);
// 由 meta data 取得作品資訊。
extract_work_data(work_data, html);
Object.assign(work_data, {
author : work_data.漫画作者 || work_data.漫畫作者 || work_data.作者
|| work_data.原著作者,
status : work_data.漫画状态 || work_data.漫畫狀態 || work_data.状态,
last_update : work_data.更新时间 || work_data.时间,
latest_chapter : work_data.最新 || work_data.更新至
|| get_label(html.between('<span class="text">更新至',
// for 36mh.js: "更新至:", 999comics.js: "更新至:"
'</span>').replace(/^[:]/, '')),
latest_chapter_url : html.between('最新:<a href="', '"')
// for 36mh.js
|| html.between('更新至 [ <a href="', '"')
// gufengmh.js
|| html.between('更新至:</strong><a href="', '"')
});
// console.log(work_data);
if (!work_data.last_update && work_data.status) {
// for 36mh.js
var matched = work_data.status
.match(/^([\s\S]+?)最近[于於]([\s\S]+?)$/);
if (matched) {
Object.assign(work_data, {
status : matched[1],
last_update : matched[2].replace(
/^[\s\n]*\[|\][\s\n]*$/g, '').trim()
});
}
}
if (!work_data.last_update) {
// for 999comics.js
var matched = html.match(/最近[于於]([\s\S]+?)<\//);
// console.log(matched);
if (matched) {
work_data.last_update = get_label(matched[1].replace(
/^[\s\n]*\[|\][\s\n]*$/g, ''));
}
}
// console.log(work_data);
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
// console.log(work_data);
var chapter_block, PATTERN_chapter_block = html
.includes('class="chapter-body')
// <div class="chapter-category clearfix">
// <div class="chapter-body clearfix">
? /class="chapter-(body|category)[^<>]+>([\s\S]+?)<\/div>/g
// 930mh.js
// <div class="zj_list_head">...<h2>章节<em class="c_3">列表</em></h2>
// <div class="zj_list_head_px" data-key="6"><span>排序 :...</div>
// <div class="zj_list_con autoHeight">...</div>
: /class="zj_list_(con|head)[^<>]+>([\s\S]+?)<\/div>/g,
//
latest_chapter_list = work_data.chapter_list;
// reset work_data.chapter_list
work_data.chapter_list = [];
// 漫畫目錄名稱不須包含分部號碼。使章節目錄名稱不包含 part_NO。
// 將會在 function get_chapter_directory_name() 自動設定。
// work_data.chapter_list.add_part_NO = false;
while (chapter_block = PATTERN_chapter_block.exec(html)) {
// delete chapter_block.input;
// console.log(chapter_block);
if (chapter_block[1] === 'category') {
// console.log(chapter_block[2]);
// e.g., 决断地 @ gufengmh
chapter_block = chapter_block[2]
// <div class="caption pull-left"><span>章节</span></div>
// <div class="caption pull-left"><span>单话</span></div>
.match(/class="caption[^<>]+>([\s\S]+)/);
// console.log(chapter_block);
if (chapter_block) {
this.set_part(work_data, chapter_block[1]);
}
continue;
}
if (chapter_block[1] === 'head') {
// console.log(chapter_block[2]);
// 930mh.js
// e.g., http://www.duzhez.com/manhua/269/
chapter_block = chapter_block[2]
// <h2>章节<em class="c_3">列表</em></h2>
// <h2>番外篇<em class="c_3">列表</em></h2>
.between('<h2>', '<em class="c_3">列表</em>');
// console.log(chapter_block);
if (chapter_block) {
this.set_part(work_data, chapter_block);
}
continue;
}
chapter_block = chapter_block[2];
var link, PATTERN_chapter_link =
//
/<a href="([^<>"]+)"[^<>]*>([\s\S]+?)<\/a>/g;
while (link = PATTERN_chapter_link.exec(chapter_block)) {
if (link[1].startsWith('javascript:')) {
// 本站应《 》版权方要求现已屏蔽删除本漫画所有章节链接,只保留作品文字信息简介以及章节目录
continue;
}
var chapter_data = {
url : link[1],
title : get_label(link[2])
};
this.add_chapter(work_data, chapter_data);
// console.log(work_data.chapter_list);
// console.log(chapter_data);
}
}
this.check_filtered(work_data, html, get_label,
//
latest_chapter_list);
work_data.inverted_order = this.chapter_inverted_order;
// console.log(work_data.chapter_list);
// throw work_data.chapter_list.length;
},
// 注意:在呼叫本函數之前,不可改變 html
check_filtered : function(work_data, html, get_label,
latest_chapter_list) {
// console.log(work_data);
// console.log(work_data.chapter_list);
var text = work_data.chapter_list.length === 0 && get_label(
/**
* 已屏蔽删除本漫画所有章节链接 e.g., <code>
// 930mh.js 一人之下
<div class="zj_list_con autoHeight">
<p class="ip-notice" style="padding:10px;color: red;background:snow;font-size:14px;width:875px;">
尊敬的各位喜爱一人之下漫画的用户,本站应《一人之下》版权方要求现已屏蔽删除本漫画所有章节链接,只保留作品文字信息简介以及章节目录,请喜欢一人之下的漫友购买杂志或到官网付费欣赏。为此给各位漫友带来的不便,敬请谅解!
</p>
</div>
// mh1234.js
<div class="ip-body">
<p class="ip-notice">
尊敬的各位喜爱妖精种植手册漫画的用户,本站应《妖精种植手册 》版权方要求现已屏蔽删除本漫画所有章节链接,只保留作品文字信息简介以及章节目录,请喜欢妖精种植手册 的漫友购买杂志或到官网付费欣赏。为此给各位漫友带来的不便,敬请谅解!
</p>
<p>
版权方在线阅读地址: <span><a href="http://www.mh1234.com" rel="nofollow">http://www.mh1234.com</a></span>
</p>
</div>
</code>
*/
html.between('<p class="ip-notice"', '</p>').between('>')
//
|| html.between('class="ip-body">', '</div>'));
// console.log(text);
if (!text) {
return;
}
work_data.removed = text;
var chapter_id = html.between('href="/comic/read/?id=', '"')
|| html.between('SinMH.initComic(', ')')
|| html.between('SinTheme.initComic(', ')')
|| html.between('var pageId = "comic.', '"');
if (this.try_to_get_blocked_work && chapter_id) {
library_namespace.info([ work_data.title || work_data.id, ': ',
{
// gettext_config:{"id":"trying-to-get-the-blocked-work"}
T : '嘗試取得被屏蔽的作品。'
} ]);
if (Array.isArray(latest_chapter_list)
// e.g., 全职法师, 一人之下 http://www.duzhez.com/manhua/1532/
&& latest_chapter_list.length > 1
//
&& (!this.recheck || this.recheck in {
changed : true,
multi_parts_changed : true
})) {
library_namespace.info({
// gettext_config:{"id":"using-the-previous-cache-to-download-§$1"}
T : [ '使用之前的快取,自 §%1 接續下載。',
latest_chapter_list.length ]
});
// 這可以保留 work_data.chapter_list 先前的屬性。
work_data.chapter_list = Object.assign(latest_chapter_list,
work_data.chapter_list);
work_data.last_download.chapter = latest_chapter_list.length;
} else {
this.add_chapter(work_data,
//
'/comic/read/?id=' + chapter_id);
}
} else {
library_namespace.warn(text);
}
},
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
var html = XMLHttp.responseText;
if (work_data.removed && chapter_NO === 1) {
var first_chapter_id = html.between('SinMH.initChapter(', ',')
|| html.between('SinTheme.initChapter(', ',');
// console.log(html);
if (first_chapter_id) {
library_namespace.debug('add first chapter: '
+ first_chapter_id);
var url = this.work_URL(work_data.id) + first_chapter_id
+ '.html';
work_data.chapter_list[chapter_NO - 1].url = url;
this.get_URL(url, callback, null, {
error_retry : this.MAX_ERROR_RETRY,
no_warning : true
});
return;
}
}
var crypto_url = html
// 930mh.js: Error on http://www.duzhez.com/manhua/449/245193.html
&& html
// https://www.manhuadui.com/manhua/haizeiwang/296660.html :
// <script
// src="https://cdn.staticfile.org/crypto-js/3.1.9-1/crypto-js.js"></script>
.match(/<script src="([^"]+\/crypto(?:-js)?\.js)"><\/script>/);
// console.log(crypto_url);
if (crypto_url) {
var file_name = this.main_directory + 'crypto.js';
// TODO: this is a workaround to pass to require()
if (!library_namespace.is_absolute_path(file_name)) {
file_name = process.cwd()
+ library_namespace.env.path_separator + file_name;
}
// console.log(file_name);
library_namespace.get_URL_cache(this.full_URL(crypto_url[1]),
// @see function cops201921() @
// http://www.duzhez.com/js/cops201921.js
function(data, error, XMLHttp) {
// data = data.toString();
// @see https://code.google.com/archive/p/crypto-js/
// 懶得自己寫,直接 including。
global.CryptoJS = require(file_name);
callback();
}, {
file_name : file_name,
get_URL_options : this.get_URL_options
});
return;
}
callback();
},
// 取得每一個章節的各個影像內容資料。 get_chapter_data()
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// console.log(html);
if (work_data.removed && !work_data.chapter_filtered) {
var next_chapter_data = html.between('nextChapterData =', ';');
// console.log(next_chapter_data || html);
if (next_chapter_data
// next_chapter_data==='' @
// https://www.mh1234.com/comic/9384.html
&& (next_chapter_data = JSON.parse(next_chapter_data))
&& next_chapter_data.id > 0) {
library_namespace.debug('add chapter: '
+ next_chapter_data.id);
next_chapter_data.url = this.work_URL(work_data.id)
+ next_chapter_data.id + '.html';
// 動態增加章節。
work_data.chapter_count++;
work_data.chapter_list.push(next_chapter_data);
} else {
// console.log(html);
}
}
// console.log(work_data.chapter_list);
var chapter_data = work_data.chapter_list[chapter_NO - 1],
// <!--全站头部导航 结束-->\n<script>
chapter_data_code = html
// 930mh.js: Error on http://www.duzhez.com/manhua/449/245193.html
&& (html.match(/<script>(;var [\s\S]+?)<\/script>/)
// for manhuaniu.js 2021/1/19 改版
|| html.match(/<script>(var siteName = "";[\s\S]+?)<\/script>/));
// console.trace(chapter_data_code);
if (!chapter_data_code) {
library_namespace.warn({
// gettext_config:{"id":"unable-to-parse-chapter-data-for-«$1»-§$2"}
T : [ '無法解析《%1》§%2 之章節資料!', work_data.title, chapter_NO ]
});
return;
}
// console.trace(chapter_data_code[1]);
// eval(chapter_data_code[1].replace(/;var /g, ';chapter_data.'));
chapter_data_code[1].split(';var ').forEach(function(token) {
if (!token.includes('='))
return;
token = token.replace(/^\s*var\s/, '');
// console.trace(token);
try {
eval('chapter_data.' + token);
} catch (e) {
console.error(new SyntaxError(
// Ignore SyntaxError. e.g.,
// https://www.gufengmh8.com/manhua/wodeshashounanyou/742494.html
// ;var pageTitle = "我的杀手男友第65、66话 "肉偿在线观看";
'parse_chapter_data: ' + token));
}
});
// console.log(chapter_data);
// 設定必要的屬性。
chapter_data.title = get_label(html.between('<h2>', '</h2>'))
// e.g., mh1234.js has no <h2>...</h2>'
|| chapter_data.title;
// e.g., 'images/comic/4/7592/'
var path = encodeURI(chapter_data.chapterPath);
// console.log(chapter_data.chapterImages);
if (global.CryptoJS
&& typeof chapter_data.chapterImages === 'string') {
// console.log(chapter_data.chapterImages);
// console.log(this.crypto);
/**
* <code>
JSON.parse(CryptoJS.AES.decrypt(chapterImages,CryptoJS.enc.Utf8.parse("6133AFVvxas55841"),{iv:CryptoJS.enc.Utf8.parse("A25vcxQQrpmbV51t"),mode:CryptoJS.mode.CBC,padding:CryptoJS.pad.Pkcs7}).toString(CryptoJS.enc.Utf8))
</code>
*
* @see https://segmentfault.com/q/1010000011225051
*/
chapter_data.chapterImages =
// 使用 CryptoJS https://code.google.com/archive/p/crypto-js/
// https://github.com/brix/crypto-js
JSON.parse(CryptoJS.AES.decrypt(chapter_data.chapterImages,
// 930mh.js key 密鑰 "十六位字符作为密钥"
CryptoJS.enc.Utf8.parse(this.crypto.key), {
iv : CryptoJS.enc.Utf8.parse(this.crypto.iv),
mode : CryptoJS.mode.CBC,
padding : CryptoJS.pad.Pkcs7
}).toString(CryptoJS.enc.Utf8));
}
// assert: Array.isArray(chapter_data.chapterImages)
chapter_data.image_list = chapter_data.chapterImages.map(function(
url) {
return {
// e.g., 外挂仙尊 184 第76话
// 但是這還是沒辦法取得圖片...
url : encodeURI(/^https?:\/\//.test(url) ? url
//
: path + url)
}
});
if (chapter_data.image_list.length === 0
&& (html = html.between('class="ip-notice">', '<'))) {
// 避免若連內容被屏蔽,會從頭檢查到尾都沒有成果。
work_data.chapter_filtered = true;
if (work_data.removed) {
library_namespace.info({
// gettext_config:{"id":"§$1-has-been-blocked-and-no-longer-attempts-to-resolve-other-chapters"}
T : [ '§%1 已被屏蔽,不再嘗試解析其他章節。', chapter_NO ]
});
} else {
library_namespace.warn(get_label(html));
}
}
// console.log(chapter_data);
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_SinMH_comics_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
if (configuration.search_URL === 'API') {
configuration.search_URL = default_configuration.search_URL_API;
// 因為不見得會執行到 parse_search_result(),不可放在 parse_search_result() 裡面。
if (!configuration.id_of_search_result) {
// gufengmh.js: using 'slug'
configuration.id_of_search_result = 'id';
}
configuration.title_of_search_result = 'title';
}
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
// for CeL.application.net.work_crawler.sites.SinMH2013
new_SinMH_comics_crawler.default_configuration = default_configuration;
return new_SinMH_comics_crawler;
}

View File

@@ -0,0 +1,288 @@
/**
* @name CeL module for downloading SinMH CMS? comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見漫畫管理系統: 可能是 圣樱漫画管理系统 (圣樱CMS) 2013年版 的工具。
*
* TODO: https://m.999comics.com/
*
* <code>
CeL.SinMH2013(configuration).start(work_id);
</code>
*
* modify from archive/2manhua.js
*
* 57mh 介面程式碼類似於 999comics。manhuagui 似乎是在這基礎上經過修改? 57mh 這一批介面外觀與
* CeL.application.net.work_crawler.sites.SinMH 類似,但介面程式碼有些差距。或可稱為
* CeL.application.net.work_crawler.sites.SinMH2013 或
* CeL.application.net.work_crawler.sites.SMH。
*
* @see https://www.999comics.com/static/scripts/main.js?v=1.0 MHD (MHD: 漫画岛
* http://www.manhuadao.com/book/baiqianjiadeyaoguaiwangzi/)
* http://www.wuqimh.com/templates/wuqi/default/scripts/main.js?v=1.0.3 MHW
* https://cf.hamreus.com/scripts_tw/main_EB87BCACAD66FA68CA738D0C925DC508.js
* main_EB87BCACAD66FA68CA738D0C925DC508.js 末: SMH = { update: "2013/4/1" }
*
* @since 2019/6/18 6:13:11 模組化 MHD模板?
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/57mh.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.SinMH2013',
require : 'application.net.work_crawler.'
//
+ '|application.net.work_crawler.sites.SinMH.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// 本站常常無法取得圖片,因此得多重新檢查。
// recheck:從頭檢測所有作品之所有章節與所有圖片。不會重新擷取圖片。對漫畫應該僅在偶爾需要從頭檢查時開啟此選項。
// recheck : true,
// 當無法取得chapter資料時直接嘗試下一章節。在手動+監視下recheck時可併用此項。
// skip_chapter_data_error : true,
// 36145 前任攻略 19话 057.jpg
// {Natural}MIN_LENGTH:最小容許圖案檔案大小 (bytes)。
MIN_LENGTH : 400,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
skip_error : true,
// one_by_one : true,
// base_URL : '',
// 取得伺服器列表。
// use_server_cache : true,
// http://www.5qmh.com/templates/wuqi/default/scripts/configs.js?v=1.0.3
server_URL : 'templates/wuqi/default/scripts/configs.js',
parse_server_list : function(html) {
// console.log(html);
var server_list = [];
Object.values(JSON.parse(
// var pageConfig = { 'host': { ...
html.replace(/^[^{]+/, '').replace(/[^}]+$/, '')
//
.replace(/'/g, '"')).host)
//
.forEach(function(_server_list) {
_server_list.forEach(function(server) {
if (server) {
if (!server.endsWith('/'))
server += '/';
server_list.push(server);
}
});
});
return server_list;
},
// 解析 作品名稱 → 作品id get_work()
search_URL : 'handler/suggest?cb=_&key=',
parse_search_result : function(html) {
// console.log(html);
// e.g.,
// _([{"id":"28015","t":"民工勇者","u":"/comic/28015/","cid":"/comic/28015/0208","ct":"207话","s":"0"},{"id":"28093","t":"无敌勇者王(民工勇者)","u":"/comic/28093/","cid":"/comic/28093/02","ct":"199话","s":"0"}])
var id_data = html ? JSON.parse(html.between('(').replace(
/\)[^)]*$/, '')) : [];
return [ id_data, id_data ];
},
id_of_search_result : function(cached_data) {
return cached_data.id | 0;
},
title_of_search_result : 't',
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
// e.g., http://www.5qmh.com/28437/
return work_id + '/';
},
parse_work_data : library_namespace.SinMH.default_configuration.parse_work_data,
get_chapter_list : function(work_data, html, get_label) {
// console.log(html);
work_data.chapter_list = [];
var matched, part_title, part_NO = 0, page,
// 2017/7/22: 57mh.js 章節編號順序為 21 43 65.
// e.g., 银魂 http://www.wuqimh.com/8/
// matched: [ all, part_title, page inner ]
PATTERN_page = /<h4><span>(.+?)<\/span><\/h4>|<ul (?:style="display:block;")?>([\s\S]+?)<\/ul>/g,
/**
* e.g., 57mh.js 只發現過多page頁面沒有發現過多part <code>
<h4><span>单话</span></h4><div class="chapter-page cf mt10" id="chpater-page-1"><ul>
<li ><a href="javascript:;" title="第1页">第1页<i></i></a></li>
...
<li class="on"><a href="javascript:;" title="第7页">第7页<i></i></a></li>
</ul></div>
<div class="chapter-list cf mt10" id="chpater-list-1"><ul >
<li><a href="/8/065.html" title="155话" class="status0" target="_blank"><span>155话<i>21p</i></span></a></li>
...
</ul></div>
</code>
*
* e.g., 999comics.js 只發現過多part沒有發現過多page頁面 <code>
<h4><span>單話</span></h4>
<div class="chapter-list cf mt10">
<ul style="display:block;">
<li><a href="/comic/26060/72192e0511125993c37cbd5264c971b6.html" title="第371回" class="status0" target="_blank"><span>371回</span></a></li>
...
</ul>
</div><h4><span>番外篇</span></h4>
<div class="chapter-list cf mt10">
<ul style="display:block;">
<li><a href="/comic/26060/b0da0b7c0767f1332684a0ae111b3696.html" title="Jump next出張篇" class="status0" target="_blank"><span>Jump</span></a></li>
...
</ul>
</div>
</code> 單話 番外篇 單行本
*
* <code>
<li><a href="/comic/25652/072.html" title="72回 碧霞坠" class="status0" target="_blank"><span>72回<i>14p</i></span></a></li>
</code>
*/
PATTERN_chapter =
// matched: [ all, href, title, inner ]
/<li><a href="([^"<>]+)" title="([^"<>]+)"[^<>]*>(.+?)<\/a><\/li>/g;
while (page = PATTERN_page.exec(html)) {
if (page[1]) {
part_title = get_label(page[1]);
part_NO++;
// library_namespace.info('part_title: ' + part_title);
continue;
}
page = page[2];
// console.log(page);
var chapter_list = [];
while (matched = PATTERN_chapter.exec(page)) {
matched[2] = matched[2].trim();
if (matched[3] = matched[3].between('<i>', '</i>')) {
// add page count
matched[2] = matched[2] + ' ' + matched[3];
}
chapter_list.push({
part : part_title,
part_NO : part_NO,
title : get_label(matched[2]),
url : encodeURI(matched[1])
});
}
if (!this.no_need_to_revert)
chapter_list = chapter_list.reverse();
work_data.chapter_list.append(chapter_list);
}
work_data.chapter_list.part_NO = part_NO;
return;
// 已被棄置的排序方法。
work_data.chapter_list
.sort(function(chapter_data_1, chapter_data_2) {
var matched_1 = chapter_data_1.url.match(/(\d+)\.htm/),
// 依照.url排序。
matched_2 = chapter_data_2.url.match(/(\d+)\.htm/);
if (matched_1 && matched_2) {
return matched_1[1] - matched_2[1];
}
return chapter_data_1.url < chapter_data_2.url ? -1 : 1;
// 依照.title排序。
return chapter_data_1.title < chapter_data_2.title ? -1
: 1;
});
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// decode chapter data
function decode(code) {
code = eval(code).replace(/^[^=]+/, 'code');
return eval(code);
}
var chapter_data = html
.match(/[>;\n\s]var\s+cInfo\s*=\s*(\{[\s\S]+?\})[;\n]/);
if (chapter_data) {
// 999comics.js
// console.log(chapter_data[1]);
eval('chapter_data=' + chapter_data[1]);
// https://www.999comics.com/static/scripts/core2.js?v=20180206
// preload: function(t) {...}
// r("<img />")[0].src = "//www.999comics.com/g.php?"+o.cid+'/'+
// o.fs[i - 1]
chapter_data.fs = chapter_data.fs.map(function(i) {
return this.full_URL('g.php?'
//
+ chapter_data.cid + '/' + i);
}, this);
} else if (chapter_data = html.between(
'<script type="text/javascript">eval', '\n')) {
// 57mh.js
chapter_data = decode(chapter_data);
}
if (!chapter_data) {
library_namespace.warn({
// gettext_config:{"id":"unable-to-parse-chapter-data-for-«$1»-§$2"}
T : [ '無法解析《%1》§%2 之章節資料!', work_data.title, chapter_NO ]
});
return;
}
// console.log(chapter_data);
// 設定必要的屬性。
chapter_data.title = get_label(html.between('<h2>', '</h2>'));
chapter_data.image_count = chapter_data.fc;
chapter_data.image_list = chapter_data.fs;
if (!chapter_data.fs.at(-1)) {
// for http://www.5qmh.com/6908/0296.html?p=9
chapter_data.fs.pop();
chapter_data.image_count--;
}
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_SinMH2013_comics_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
return new_SinMH2013_comics_crawler;
}

View File

@@ -0,0 +1,168 @@
/**
* @name CeL module for downloading YOUNG ACE UP, TYPE-MOON comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 KADOKAWA CORPORATION webエース ヤングエースUPアップ
* Webコミック、TYPE-MOONコミックエース 漫画 的工具。
*
* <code>
CeL.ace({
// configuration
site : '' || CeL.get_script_name()
}).start(work_id);
</code>
*
* @since 2020/4/26 5:58:57 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.ja-JP/youngaceup.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.ace',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 日本的網路漫畫網站習慣刪掉舊章節,因此每一次都必須從頭檢查。
// e.g., ヱデンズボゥイ
recheck : true,
// one_by_one : true,
base_URL : 'https://web-ace.jp/',
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return 'contents/' + work_id + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
var work_data = JSON.parse(html.between(
'<script type="application/ld+json">', '</script>'));
extract_work_data(work_data, html);
// 放在這裡以預防被extract_work_data()覆蓋。
Object.assign(work_data, {
// 必要屬性:須配合網站平台更改。
title : get_label(html.between('<h1>', '</h1>')),
authors : html.all_between('<p class="author">', '</p>').map(
get_label),
// 選擇性屬性:須配合網站平台更改。
subtitle : get_label(html.between('<p class="subtitle">',
'</p>')),
description : get_label(html.between(
'<div class="description">', '</div>')),
status : html.between('<p class="genre">', '</p>').replace(
'ジャンル:', '').split(' / ').map(get_label),
last_update : get_label(html.between(
'<span class="updated-date">', '</span>'))
|| (new Date).toISOString(),
next_update : html.all_between(
'<span class="label_day-of-the-week">', '</span>').map(
get_label)
// 隔週火曜日更新 次回更新予定日2018年11月27日
.map(function(token) {
return token.replace('次回更新予定日:', '');
})
});
work_data.author = work_data.authors.map(function(name) {
// 原作: 漫画: キャラクター原案:
return name.replace(/^[^]+/, '').trim();
});
// console.log(work_data);
return work_data;
},
chapter_list_URL : function(work_id, work_data) {
return this.work_URL(work_id) + 'episode/';
},
get_chapter_list : function(work_data, html, get_label) {
// <div class="container" id="read">
html = html.between(' id="read">', '</section>')
// <ul class="table-view">
.between('<ul', '</ul>');
work_data.chapter_list = [];
var some_skipped;
html.each_between('<li', '</li>', function(token) {
var matched = token.between('<p class="yudo_wa">', '</div>');
if (matched) {
library_namespace.info(work_data.title + ': '
+ get_label(matched).replace(/\s{2,}/g, ' '));
some_skipped = true;
return;
}
matched = token
.match(/<a [^<>]*?href=["']([^"'<>]+)["'][^<>]*>/);
var chapter_data = {
title : get_label(token
//
.between('<p class="text-bold">', '</p>')),
date : token.between('<span class="updated-date">',
'</span>'),
// 直接取得圖片網址資訊。
url : matched[1] + 'json/'
};
work_data.chapter_list.push(chapter_data);
});
work_data.chapter_list.reverse();
if (some_skipped) {
// 因為中間的章節可能已經被下架,因此依章節標題來定章節編號。
this.set_chapter_NO_via_title(work_data);
}
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// console.log(html);
var chapter_data = work_data.chapter_list[chapter_NO - 1];
Object.assign(chapter_data, {
// 設定必要的屬性。
image_list : JSON.parse(html)
});
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_ace_comics_crawler(configuration, callback, initializer) {
// library_namespace.set_debug(9);
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
configuration.base_URL += (configuration.site
// || library_namespace.get_script_name()
) + '/';
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
return crawler;
}
return new_ace_comics_crawler;
}

View File

@@ -0,0 +1,346 @@
/**
* @name CeL module for downloading baozimh comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 包子漫畫 的工具。
*
* <code>
CeL.baozimh({
// configuration
site : '' || CeL.get_script_name()
}).start(work_id);
</code>
*
* @since 2022/11/3 5:55:24
* @since 2022/11/3 5:55:24 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hant-TW/baozimh.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.baozimh',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
// ----------------------------------------------------------------------------
function module_code(library_namespace) {
// requiring
/**
* <code>
<a href="/user/page_direct?comic_id=yuanlaiwoshixiuxiandalao-luanshijiaren&amp;section_slot=0&amp;chapter_slot=0" rel="noopener" class="comics-chapters__item" data-v-0c0802bc><div style="flex: 1;" data-v-0c0802bc><span data-v-0c0802bc>預告</span></div></a>
<code>
*/
var PATTERN_chapter_link = /<a [^<>]*?href="([^<>"]+?)" [^<>]* class="comics-chapters__item"[^<>]*>([\s\S]+?)<\/a>/g;
// --------------------------------------------------------------------------------------------
var default_configuration = {
base_URL : 'https://www.baozimh.com/',
// 最小容許圖案檔案大小 (bytes)。
// 對於極少出現錯誤的網站,可以設定一個比較小的數值,並且設定.allow_EOI_error=false。因為這類型的網站要不是無法取得檔案要不就是能夠取得完整的檔案要取得破損檔案並且已通過EOI測試的機會比較少。
// 對於有些圖片只有一條細橫桿的情況。
MIN_LENGTH : 50,
// e.g., 都是黑丝惹的祸2 0409 第二季 第409话 因为我喜欢他
// allow .jpg without EOI mark.
// allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
skip_error : true,
// e.g., woshenshangyoutiaolong-pikapi 我身上有条龙/
// 0590 第590话 父母过往/woshenshangyoutiaolong-pikapi-590-017.jpg
// one_by_one : true,
acceptable_types : 'jpg|webp',
// 解析 作品名稱 → 作品id get_work()
search_URL : 'search?q=',
parse_search_result : function(html, get_label) {
html = html.between('<div class="pure-g classify-items">');
// console.log(html);
var id_list = [], id_data = [];
html.each_between('<a href="/comic/', '</a>', function(text) {
id_list.push(text.between(null, '"'));
id_data.push(get_label(text.between('title="', '"')));
});
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return 'comic/' + work_id;
},
parse_work_data : function(html, get_label, extract_work_data) {
var work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(html.between(
// <h1 class="comics-detail__title"
// data-v-6f225890>原來我是修仙大佬</h1>
'<h1 class="comics-detail__title"', '</h1>').between('>')),
author : get_label(html.between(
// <h2 class="comics-detail__author" data-v-6f225890>亂室佳人</h2>
'<h2 class="comics-detail__author"', '</h2>').between('>')),
// 選擇性屬性:須配合網站平台更改。
tags : html.all_between('<span class="tag"', '</span>')
//
.map(function(tag) {
return get_label(tag.between('>'));
}),
last_update : get_label(html.between('最新:').between('<em',
'</em>').between('>').replace(/\((.+) 更新\)/, '$1')),
latest_chapter : get_label(html.between('最新:', '</a>')),
description : get_label(html.between('<p class="comics-detail',
'</p>').between('>')),
/**
* cover image<code>
<amp-img alt="原來我是修仙大佬" width="180" height="240" layout="responsive" src="https://static-tw.baozimh.com/cover/yuanlaiwoshixiuxiandalao-luanshijiaren.jpg" data-v-6f225890>
<code>
*/
cover_image : html.between('layout="responsive" src="', '"')
};
// 由 meta data 取得作品資訊。
extract_work_data(work_data, html);
// console.log(work_data);
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
var _this = this;
// reset chapter list
work_data.chapter_list = [];
var part_count = html.all_between('<div class="section-title"',
'</div>').length;
var skip_latest_chapters = true;
html.each_between('<div class="section-title"', null,
//
function(text) {
/**
* <code>
<div class="section-title" data-v-6f225890>章節目錄</div>
<code>
*/
var part_title = text.between('>', '</div>');
// 最新章節 最新章节
if (/^最新章[節节]$/.test(part_title)
// 假如只有一個 part那就必須留下最新章節。 e.g., 妖精种植手册黑白轮回篇
&& (skip_latest_chapters = part_count > 1)) {
return;
}
_this.set_part(work_data, part_title);
// console.log(text);
var matched;
while (matched = PATTERN_chapter_link.exec(text)) {
var chapter_data = {
// reset sub_chapter_list
sub_chapter_list : null,
title : get_label(matched[2]),
// TODO: fix "&amp;"
url : matched[1].replace(/&amp;/g, '&')
};
_this.add_chapter(work_data, chapter_data);
}
});
// console.trace([ part_count, skip_latest_chapters ]);
if (!skip_latest_chapters) {
// 最新章節 為倒序。
// e.g., 妖精种植手册黑白轮回篇
// https://cn.baozimh.com/comic/yaojingchongzhishouceheibailunhuipian-dazui
// 我独自升级
// https://www.baozimh.com/comic/woduzishengji-duburedicestudio_n6ip31
work_data.inverted_order = true;
}
// console.log(work_data.chapter_list);
},
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
// console.log(XMLHttp);
// console.log(work_data);
// 模擬歸一化
// https://www.webmota.com/comic/chapter/wangyouzhijinzhanfashi-samanhua/0_188.html
// https://cn.webmota.com/comic/chapter/wangyouzhijinzhanfashi-samanhua/0_188.html
function simulated_normalized_url(url) {
return url.replace(/\/\/[a-z]+\./, '//www.');
}
var chapter_data = work_data.chapter_list[chapter_NO - 1];
// console.trace(chapter_data);
if (!chapter_data.sub_chapter_list) {
// get_chapter_list() 獲得的是動態的 url會轉成靜態的。
chapter_data.sub_chapter_list = [ XMLHttp.responseURL ];
}
if (chapter_data.static_url) {
if (simulated_normalized_url(chapter_data.static_url) !== simulated_normalized_url(chapter_data.sub_chapter_list[0])) {
// console.log(chapter_data);
library_namespace.warn('§' + chapter_NO + '《'
+ chapter_data.title
+ '》: 從上一章的章節內容頁面獲得的 URL 不同於從章節列表獲得的 URL\n '
+ chapter_data.static_url + '\n '
+ chapter_data.sub_chapter_list[0]);
}
// free
delete chapter_data.static_url;
}
var html = XMLHttp.responseText, matched, next_chapter_url = html;
// console.log(html);
/**
* <code>
// 可能有上下兩個 `<div class="next_chapter">`,取後一個。
<div class="chapter-main scroll-mode"><div class="next_chapter"><a href="https://cn.webmota.com/comic/chapter/dubuxiaoyao-zhangyuewenhua/0_35.html#bottom">
点击进入上一页
</a></div>
<div class="next_chapter"><a href="https://www.webmota.com/comic/chapter/shenzhita-siu/0_738_2.html">
點擊進入下一頁
<span class="iconfont icon-xiangxia"></span></a></div>
<div class="next_chapter"><a href="https://cn.webmota.com/comic/chapter/shenzhita-siu/0_738.html">
点击进入下一话
<span class="iconfont icon-xiayibu"></span></a></div>
<code>
*/
while (matched = next_chapter_url.between(' class="next_chapter">'))
next_chapter_url = matched;
next_chapter_url = next_chapter_url
// 去掉網頁錨點。
&& next_chapter_url.between(' href="', '"').replace(/#.*$/, '');
if (!next_chapter_url
//
|| !/^_\d/.test(simulated_normalized_url(next_chapter_url)
// 確定 url 以本章節 url 開頭。
.between(simulated_normalized_url(
//
chapter_data.sub_chapter_list[0]).replace(/\.html$/, '')))) {
// console.trace('下一話');
// assert: next_chapter_url 為下一話的靜態 url。
var next_chapter_data = work_data.chapter_list[chapter_NO];
if (next_chapter_url && next_chapter_data) {
// 做個記錄。
if (false) {
console.trace([ chapter_NO, next_chapter_url,
next_chapter_data ]);
}
next_chapter_data.static_url = next_chapter_url;
// 直接從靜態網頁獲取章節內容,避免採用 CGI。
if (!next_chapter_data.sub_chapter_list)
next_chapter_data.sub_chapter_list = [ next_chapter_url ];
}
// console.trace(chapter_data);
callback();
return;
}
// assert: next_chapter_url 為下一頁的靜態 url。
// console.trace('下一頁');
// 做個記錄。
chapter_data.sub_chapter_list.push(next_chapter_url);
// console.trace(next_chapter_url);
this.get_URL(next_chapter_url, function(XMLHttp, error) {
if (!chapter_data.next_chapter_HTMLs)
chapter_data.next_chapter_HTMLs = [];
chapter_data.next_chapter_HTMLs.push(XMLHttp.responseText);
this.pre_parse_chapter_data(XMLHttp, work_data, callback,
chapter_NO);
});
},
// 取得每一個章節的各個影像內容資料。 get_chapter_data()
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var chapter_data = work_data.chapter_list[chapter_NO - 1];
var image_list = chapter_data.image_list = [], url_Set = new Set;
function handle_html(html) {
html = html
// <div class="chapter-main scroll-mode">
.between('<div class="chapter-main scroll-mode">');
// console.trace(html);
/**
* <code>
<img src="https://s1.baozimh.com/scomic/yuanlaiwoshixiuxiandalao-luanshijiaren/0/0-vmac/1.jpg" alt="原來我是修仙大佬 - 預告 - 1" width="1200" height="3484" data-v-25d25a4e>
<code>
*/
html.each_between('<img src="', '>', function(text) {
var url = encodeURI(text.between(null, '"'));
if (url_Set.has(url)) {
// 前面的部分會重複3張圖片。
return;
}
url_Set.add(url);
image_list.push({
title : get_label(text.between('alt="', '"')),
url : url
});
});
}
handle_html(html);
if (chapter_data.next_chapter_HTMLs) {
chapter_data.next_chapter_HTMLs.forEach(handle_html);
// free
delete chapter_data.next_chapter_HTMLs;
}
// console.log(image_list);
return chapter_data;
},
is_limited_image_url : function(image_url, image_data) {
// e.g.,
// https://cn.webmota.com/comic/chapter/zunshang-mankewenhua_d/0_220.html
if (typeof image_url === 'string'
// https://static-tw.baozimh.com/cover/404.jpg
&& image_url.endsWith('/cover/404.jpg')) {
return '404 Not Found';
}
}
};
// --------------------------------------------------------------------------------------------
function new_baozimh_comics_crawler(configuration, callback, initializer) {
// library_namespace.set_debug(9);
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
return crawler;
}
return new_baozimh_comics_crawler;
}

View File

@@ -0,0 +1,744 @@
/**
* @name CeL module for downloading comico comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 韓國 NHN comico Corp. 漫畫 的工具。
*
* 2021/12/22 改版
*
* <code>
CeL.comico(configuration, function(crawler) {
start_crawler(crawler, typeof module === 'object' && module);
}, function(crawler) {
setup_crawler(crawler, typeof module === 'object' && module);
});
</code>
*
* @see http://comico.kr/
*
* @since 2018/8/19 5:49:8 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/comico.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.comico',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
function add_navigation_data(data, html) {
var navigation_data;
try {
navigation_data = JSON.parse(html.between(
// コミコ 日文版有時 json 結構有問題。
// e.g., http://www.comico.jp/articleList.nhn?titleNo=3410
'<script type="application/ld+json">', '</script>')
// e.g., http://www.comico.com.tw/2870/17/
.replace(/\t+"/g, '"'));
} catch (e) {
// TODO: handle exception
}
return Object.assign(data, navigation_data);
}
var PATTERN_work_info = /<(p|div) class="[^<>"]+?__(author|(?:sub-)?description|meta)">([\s\S]+?)<\/\1>/g,
// assert: (NO_ticket_notified>=0) === false
// gettext_config:{"id":"no-read-volumes-are-available"}
NO_ticket_notified = '已無閱讀卷可用。', auto_use_ticket_notified,
// 可以用閱讀卷閱讀的章節。
READABLE_FLAG = 'W',
//
default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// e.g., 20099 俺のメンタルは限界です\0003 3話 「マンガを描く原点」\20099-3-022.jpg
MIN_LENGTH : 180,
// one_by_one : true,
base_URL : '',
// have already read the chapter
set_downloaded_if_read : true,
convert_id : {
// switch
// 警告: 需要自行呼叫 insert_id_list(id_list);
adult : function(insert_id_list, get_label) {
// TW only: 此前被當作是一般作品。
library_namespace.info([ this.id + ': ', {
// gettext_config:{"id":"subsequent-titles-of-the-work-are-considered-to-be-web-limited-works"}
T : '此後的作品標題都被當作是網頁限定作品。'
} ]);
// webonly
this.adult = true;
insert_id_list();
}
},
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
var url = (this.adult ? 'webonly/' : '')
// ↑ webonly, オトナ限定: TW only
+ 'search/index.nhn?searchWord='
+ encodeURIComponent(work_title.replace(/\s+\d+$/, ''));
if (this.base_URL.includes('\/\/plus.comico')) {
url = this.base_URL
.replace('\/\/plus.comico', '\/\/www.comico')
+ url;
}
return url;
},
// 每個項目的<li>開頭。
search_head_token : '<li class="list-article02__item">',
// title 不能用 [^<>"]+ : for case of "薔薇的嘆息 <薔薇色的疑雲 I>"
PATTERN_search : /<a href="[^<>"]*?\/(\d+)\/"[^<>]*? title="([^"]+)"/,
parse_search_result : function(html, get_label) {
// console.log(html);
html = html.between(' id="officialList">') || html;
// console.log(html);
var _this = this, id_list = [], id_data = [];
html.each_between(this.search_head_token, '</li>', function(token) {
// console.log(token);
var matched = token.match(_this.PATTERN_search);
// console.log(matched);
if (matched) {
// コミコ有些整本賣的作品,而非一話一話。
id_list.push(matched[1]);
id_data.push(get_label(matched[2]));
}
});
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return work_id + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
if (this.archive_old_works) {
// 因為不會遍歷所有章節檔案,得到的是錯誤的 `work_data.last_file_modified_date`。
// 因此必須避免執行 check_and_archive_old_work()。
library_namespace.warn([ this.id + ': ', {
// gettext_config:{"id":"this-website-does-not-support-the-function-of-archiving-old-works-(.archive_old_works)"}
T : '本網站不支援封存舊作品功能 (.archive_old_works)'
} ]);
this.archive_old_works = false;
}
var cmnData = html.between('var cmnData =', '</script>'), matched;
if (!cmnData) {
// 公式作品の「掲載終了日」について、お知らせいたします。
// 出版社の都合により、以下2作品を掲載終了とさせていただきます。
// 更新中の掲載終了につき、大変ご迷惑をおかけし申し訳ございません。
// 下記の公式作品は既に掲載終了しています。
matched = get_label(html.between(
// <p class="m-section-error__heading">お探しのページは存在しません</p>
'<p class="m-section-error__heading">', '</p>'));
if (matched) {
throw new Error(matched);
}
}
eval('cmnData=' + cmnData);
var title = (html.between('<h1 class="article', '</h1>') || html
.between('<h1', '</h1>')).between('>'), tags = [];
/**
* e.g., http://www.comico.com.tw/challenge/3263/ 去除label
*
* <code>
<h1 class="article-hero03__ttl"><i class="i-label i-label--fill article-hero03__ttl-icon">精選</i><span class="o-hidden _challengeTitle">小惡魔與草莓男友</span></h1>
</code>
*/
if (title.includes('</i>')) {
tags.push(get_label(title.between(null, '</i>')));
title = title.between('</i>');
}
var work_data = {
// 必要屬性:須配合網站平台更改。
// <h1 class="article-hero05__ttl">美麗的代價</h1>
title : get_label(title),
// 選擇性屬性:須配合網站平台更改。
tags : tags
};
extract_work_data(work_data, html);
// e.g., '<li class="article-hero03__list-tag-item">中篇故事</li>'
html.each_between('list-tag-item">', '</li>', function(text) {
tags.push(get_label(text));
});
// JavaScript Object Notation for Linked Data 關聯的資料
matched = html.between('<script type="application/ld+json">',
'</script>');
if (matched) {
// Structured Data 結構化資料
// https://search.google.com/structured-data/testing-tool
try {
matched = JSON.parse(matched);
work_data.linked_data = matched;
} catch (e) {
// library_namespace.error(matched);
}
}
while (matched = PATTERN_work_info.exec(html)) {
if (matched[3] = get_label(matched[3]).replace(/\t/g, ''))
work_data[matched[2]] = matched[3];
}
Object.assign(add_navigation_data(work_data, html),
// 警告: 這會留下個人資訊!
cmnData, {
// 更新日期:每週連載時間/是否為完結作品。 e.g., 完結作品, 每週六, 隔週週日
status : get_label(html.between('__info-value">', '</dd>')
// コミコ e.g., 完結作品, 毎週金曜日
|| html.between('<span class="o-txt-bold">', '</span>'))
.replace(/[\s\n]{2,}/g, ' '),
// 可用的閱讀券數量。
ticket_left : (cmnData.eventRentalTicket || 0)
// 若是不用等的話,表示已收到閱讀券,還有一張可用。
+ (cmnData.time && cmnData.time.leftTime === 0 ? 1 : 0),
last_checked : null
});
if (cmnData.time && cmnData.time.leftTime > 0) {
library_namespace.info({
// gettext_config:{"id":"the-next-time-you-receive-a-reading-voucher-you-will-need-$1"}
T : [ '下次收到閱讀券還要 %1。',
// レンタル券で無料 レンタル券が届きました1日で回復
// 作品を1話レンタルできます
library_namespace.age_of(0, 1000 * cmnData.time.leftTime) ]
});
}
// console.log(work_data);
return work_data;
},
chapter_list_URL : function(work_id, work_data) {
// console.log(work_data);
// library_namespace.set_debug(9);
var api = work_data.api && work_data.api.articleListAPI;
if (!api) {
api = work_data.isOfficial === false
// 2019/10: 'api/getArticleListAll.nhn' 沒有 .freeFlg
// 標記,無法自動使用閱讀卷。
// 但是對新手村作品如 '3729 神光拜達摩' 來說,
// 用 'api/getArticleListAll.nhn' 才能取得所有作品之列表。
//
// https://github.com/kanasimi/work_crawler/issues/384
// 第一次執行時,尚未取得 .isOfficial 標記,必須先採用 api/getArticleList.nhn
? 'api/getArticleListAll.nhn'
// 2019/9: 'api/getArticleList.nhn'
: 'api/getArticleList.nhn'
}
return [ api, {
titleNo : work_id
} ];
},
get_chapter_list : function(work_data, html, get_label) {
if (!Array.isArray(work_data.downloaded_chapter_list))
work_data.downloaded_chapter_list = [];
// console.log(html);
var recerse_count = 0;
html = JSON.parse(html);
html = html.result;
// for 'api/getArticleList.nhn', there is no .totalPageCnt
if (('totalPageCnt' in html) && html.totalPageCnt !== 1) {
console.log(html);
throw new Error(work_data.title + ': ' + 'Total page is '
+ html.totalPageCnt + ', not 1!');
}
// 作品改變 titleNo 時,舊 id 可能會回傳 `{"result":{}}`
html.list.forEach(function(chapter_data, index) {
chapter_data.url = chapter_data.articleDetailUrl;
// 原先都將標題設在 subtitletitle 沒東西。
chapter_data.title = get_label(chapter_data.subtitle);
if (this.set_downloaded_if_read
&& !work_data.downloaded_chapter_list[index]) {
work_data.downloaded_chapter_list[index]
// 記錄是否已經下載過本章節。
= chapter_data.read;
}
if (index > 0) {
recerse_count += Math.sign(html.list[index - 1].articleNo
- chapter_data.articleNo);
}
}, this);
work_data.chapter_list = html.list;
// 預防尾大不掉。
delete html.list;
// console.log(recerse_count);
if (recerse_count > 0) {
library_namespace.info([ work_data.title + ': ', {
// gettext_config:{"id":"change-the-list-of-reversed-chapters-to-positive-order"}
T : '將倒序章節列表轉為正序。'
} ]);
work_data.chapter_list.reverse();
}
Object.assign(work_data, html);
// 先檢查是不是還有還有沒讀過的章節。
if (work_data.ticket_left > 0) {
if (work_data.last_download) {
work_data.chapter_list.some(function(chapter_data, index) {
if (++index >= work_data.last_download.chapter) {
return true;
}
if (!chapter_data.freeFlg) {
throw new Error(this.id + ': '
+ '網站改版?未發現 .freeFlg');
}
if (!work_data.downloaded_chapter_list[index]
&& chapter_data.freeFlg === READABLE_FLAG) {
library_namespace.info([ work_data.title + ': ', {
// gettext_config:{"id":"there-are-still-$1-reading-volume-but-$2-$3-chapter-has-not-been-downloaded-yet.-so-checking-from-this-chapter"}
T : [ '還有%1張{{PLURAL:%1|閱讀卷}},且第 %2/%3 章還有沒下載過,從此章開始檢查。',
//
work_data.ticket_left, index,
//
work_data.chapter_list.length ]
} ]);
work_data.last_checked
// 記錄最後檢查過的章節。
= work_data.last_download.chapter;
work_data.last_download.chapter = index;
return true;
}
});
} else {
work_data.recheck = true;
}
}
},
consume_url : 'consume/index.nhn',
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
// console.log(work_data);
var chapter_data = work_data.chapter_list[chapter_NO - 1],
//
skip_chapter = !chapter_data.price || chapter_data.isPurchased && {
// gettext_config:{"id":"«$1»-has-been-purchased"}
T : [ '已購買章節《%1》。', chapter_data.title ]
};
// console.log(chapter_data);
if (!skip_chapter && chapter_data.expireDate > 0) {
skip_chapter = {
// gettext_config:{"id":"you-can-read-«$3»-in-this-section-before-$1-(and-$2)"}
T : [ '在 %1 之前(還有 %2可以閱讀本章節《%3》。',
//
new Date(chapter_data.expireDate).format('%m/%d %H:%M'),
//
library_namespace.age_of(Date.now(),
//
chapter_data.expireDate), chapter_data.title ]
};
}
if (!skip_chapter
&& work_data.downloaded_chapter_list[chapter_NO - 1]) {
// TODO: 應該檢查是不是真的有圖片檔案存在。若有檔案不見,
// 或者有損壞檔案,
// 那麼就把 work_data.downloaded_chapter_list[index] 設成 false。
skip_chapter = {
// gettext_config:{"id":"the-section-«$1»-has-been-downloaded-before-and-will-not-be-re-purchased"}
T : [ '之前已下載過章節《%1》不再重新購買。', chapter_data.title ]
};
}
if (!skip_chapter && chapter_data.freeFlg !== READABLE_FLAG) {
// N: TW: 本章節需要錢(coin)來閱讀。
if (chapter_data.freeFlg === 'N'
// P: JP: 本章節需要錢(30コイン) or point(15ポイント)來閱讀。
|| chapter_data.freeFlg === 'P') {
skip_chapter = true;
} else {
skip_chapter = {
// gettext_config:{"id":"the-status-of-this-chapter-is-unknown-($1).-skipping-$1-does-not-use-reading-volumes"}
T : [ '本章節狀況不明(%1)。跳過《%1》不採用閱讀卷。',
chapter_data.freeFlg, chapter_data.title ]
};
}
}
if (!skip_chapter && !(work_data.ticket_left > 0)) {
if (work_data.ticket_left !== NO_ticket_notified) {
work_data.ticket_left = NO_ticket_notified;
skip_chapter = [ {
T : NO_ticket_notified
}, {
T : [ '跳過《%1》不使用閱讀券。', chapter_data.title ]
} ];
} else
skip_chapter = true;
}
if (!skip_chapter && !this.auto_use_ticket) {
skip_chapter = auto_use_ticket_notified ? true
// @see https://github.com/kanasimi/work_crawler
: {
// gettext_config:{"id":"the-tool-is-not-set-to-automatically-use-the-reading-volume.-if-you-are-not-using-the-installation-package-and-want-to-have-the-tool-automatically-use-the-reading-volume-please-open-the-file-manager"}
T : '未設定讓本工具自動使用閱讀卷。若您並非使用安裝包並想要讓本工具自動使用閱讀卷請打開檔案總管到安裝本工具的目錄下若是您使用安裝包就不能夠設定帳號密碼了。在「work_crawler.configuration.js」這個 .js 組態檔案中設定好帳號密碼資料並設定「auto_use_ticket:true」。您可以參考 work_crawler.default_configuration.js 這個檔案來做設定。'
};
auto_use_ticket_notified = true;
}
if (skip_chapter) {
if (skip_chapter !== true) {
if (!Array.isArray(skip_chapter)) {
skip_chapter = [ skip_chapter ];
}
skip_chapter.unshift(work_data.title, ': ');
library_namespace.info(skip_chapter);
}
callback();
return;
}
// http://www.comico.com.tw/notice/detail.nhn?no=751
library_namespace.info([ work_data.title + ': ', {
// gettext_config:{"id":"reading-«$1»-with-a-reading-voucher"}
T : [ '用閱讀券閱讀《%1》。', chapter_data.title ]
} ]);
var _this = this, html = XMLHttp.responseText;
this.get_URL(this.consume_url, function(XMLHttp) {
// console.log(XMLHttp.responseText);
var matched = XMLHttp.responseText && XMLHttp.responseText
// var msg = '很抱歉,帳號需要通過電話認證才可購買。';
.match(/[\s;]msg *= *'([^']+)/);
if (matched) {
library_namespace
.error(work_data.title + ': ' + matched[1]);
// 歸零。
work_data.ticket_left = 1;
}
if (--work_data.ticket_left === 0
// 僅僅下載有閱讀券的章節,然後就回到最後讀取的章節。
&& work_data.last_checked > 0) {
// 回到原先應該檢查的章節號碼。
work_data.jump_to_chapter = work_data.last_checked;
delete work_data.last_checked;
}
// XMLHttp 只是一個轉址網頁,必須重新擷取網頁。
_this.get_URL(chapter_data.url, callback);
}, {
titleNo : work_data.id,
articleNo : chapter_data.articleNo,
// K: 專用閱讀券, P: 用point, C: 用coin購買
paymentCode : 'K',
// 使用coin時才需要
// https://github.com/zhongfly/comico/blob/master/comico.py
// ['http://www.comico.com.tw/consume/coin/publish.nhn',{paymentCode:'C'}]
// JSON.parse(result).result.coinUseToken
coinUseToken : '',
// 5, 6, コミコ:36, ...?
productNo : html
//
.between(' name="productNo" value="', '"') || 5,
// coin price
price : chapter_data.price,
// 用coin租用價格一般能租用的都可以用閱讀券。 コミコ: 20
rentalPrice : html.between(' name="rentalPrice" value="', '"')
|| '',
// point price, コミコ 漫畫作品無此項, TW only
pointRentalPrice : html.between(
' name="pointRentalPrice" value="', '"') || 120
});
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// http://static.comico.com.tw/tw/syn/spn/js/manga/article/plusMangaDetailApp/app.1.12.0.js
var chapter_data = work_data.chapter_list[chapter_NO - 1],
//
cmnData = html.between('var cmnData =', '</script>');
// console.log(cmnData);
if (cmnData) {
eval('cmnData=' + cmnData);
} else if (cmnData = html.between(
// e.g., http://plus.comico.jp/manga/24517/8/
'<p class="m-section-error__heading">', '</p>')) {
var message = work_data.title + ' §' + chapter_NO + ' '
+ chapter_data.title + ': ' + cmnData;
if (cmnData !== 'お探しのページは存在しません') {
throw new Error(message);
}
library_namespace.error(message);
return chapter_data;
}
var matched, image_url_list = html.between(
// TW: <div class="locked-episode__kv _lockedEpisodeKv"
// コミコ: <div class="locked-episode locked-episode--show-kv">
'<div class="locked-episode', '</div>');
if (image_url_list) {
chapter_data.limited = true;
// TW: style="background-image: url('...');">
image_url_list = image_url_list.between("url('", "'")
// コミコ: <img src=".jpg" width="88" height="88" alt="" />
|| image_url_list.between(' src="', '"');
// 日文版plus.comico 此時雖然有 {Array}cmnData.imageData
// 但缺少hash而不能取得。
if (/\.jpg$/.test(image_url_list)) {
// 日文版plus.comico 此時僅有一個縮圖可用,跳過不取。
cmnData.imageData = [];
} else if (cmnData.imageData
&& cmnData.imageData.filter(function(url) {
return url && !/\.jpg$/.test(url);
}).length > 0) {
// 應該不會到這裡來了。
cmnData.imageData.unshift(image_url_list);
} else {
// 中文版的狀況。
cmnData.imageData = [ image_url_list ];
}
} else if (image_url_list = html
// comico_jp: <div class="comic-image _comicImage">
// e.g., 新手村
.between(' _comicImage">', '</div>')) {
// 一般正常可取得圖片的情況。
// 去除 placeholder。 <div class="comic-image__blank-layer">
image_url_list = image_url_list.between(null, '<div ')
|| image_url_list;
image_url_list = image_url_list.all_between(' src="', '"');
// assert: {Array}image_url_list
if (cmnData.imageData) {
// 中文版, 日文版plus.comico 將除第一張外所有圖片網址放在
// {Array}cmnData.imageData 裡面。
if (image_url_list.length === 1) {
cmnData.imageData.unshift(image_url_list[0]);
} else if (
/**
* e.g.,
* http://www.comico.jp/detail.nhn?titleNo=27605&articleNo=1
* <code>
<div class="swiper-wrapper _swiperWrapper _comicImage">
<!-- Slides -->
<div class="swiper-slide _swiperSlide o-hidden">
<div dir="ltr">
<div id="_popIn_video"></div>
</code>
*/
!html.includes(' class="swiper-slide _swiperSlide')) {
// gettext_config:{"id":"web-page-revision?-unable-to-parse"}
var message = '網頁改版?無法解析!';
if (library_namespace.gettext)
message = library_namespace.gettext(message);
throw new Error('parse_chapter_data: '
+ work_data.title + ' §' + chapter_NO + ': '
+ message);
Array.prototype.unshift.apply(cmnData.imageData,
image_url_list);
}
} else {
// コミコ 日文版一般漫畫將所有圖片放在這之間,無 cmnData.imageData。
cmnData.imageData = image_url_list;
}
} else if (matched = html
// e.g., https://www.comico.jp/detail.nhn?titleNo=4235&articleNo=219
.match(/<h2 class="[^"<>]*o-txt-error[^"<>]*">([\s\S]+?)<\/h2>/)) {
var message = get_label(matched[1]);
throw new Error(work_data.title + ' §' + chapter_NO + ': '
+ message);
} else if (html.includes('<p class="error-section__ttl">')) {
var message = get_label(html.between(
/**
* <code>
<div class="error-section o-mt100 o-mb100">
<p class="error-section__ttl">無法閱讀</p>
<p class="error-section__description">本話正在審查當中。<br/>審查完成後將會開放閱讀</p>
<p class="o-mt30"><a href="" class="btn03" onclick="javascript:history.back();">離開</a></p>
<!-- /.m-section-error --></div>
</code>
*/
'<p class="error-section__ttl">', '</p>'))
//
+ ': ' + get_label(html.between(
//
'<p class="error-section__description">', '</p>'));
throw new Error(work_data.title + ' §' + chapter_NO + ': '
+ message);
} else {
console.log(html);
// gettext_config:{"id":"web-page-revision?-unable-to-parse"}
var message = '網頁改版?無法解析!';
if (library_namespace.gettext)
message = library_namespace.gettext(message);
throw new Error(work_data.title + ' §' + chapter_NO + ': '
+ message);
}
if (cmnData.url) {
// e.g., http://plus.comico.jp/manga/24529/13/
// 預防 chapter_data.url 被污染。
cmnData._url = cmnData.url;
delete cmnData.url;
}
// console.log(work_data);
// console.log(chapter_data);
// console.log(cmnData);
Object.assign(add_navigation_data(chapter_data, html), {
// 設定必要的屬性。
image_list : cmnData.imageData.map(function(url) {
if (chapter_data.limited
// http://comicimg.comico.jp/tmb/00000/1/hexhex_hexhexhex.jpg"
&& url.includes('.jp/tmb/') && /\.jpg$/.test(url))
return;
// chapter_data.isOfficial ? 官方作品 : 新手村
if (chapter_data.isOfficial && chapter_data.freeFlg !== 'Y'
// 付費章節: 中文版提供第一張的完整版,日文版只提供縮圖。
// 圖片都應該要有hash且不該是縮圖。
&& (url.includes('.jp/tmb/') || /\.jpg$/.test(url))) {
var message = library_namespace.gettext
// gettext_config:{"id":"invalid-image-url-$1"}
? library_namespace.gettext('Invalid image url: %1',
url) : 'Invalid image url: ' + url;
throw new Error(work_data.title + ' §' + chapter_NO
+ ' ' + chapter_data.title + ': ' + message);
}
return {
url : url
};
})
}, cmnData);
return chapter_data;
},
// @see work_crawler_loader.js
after_download_list : function() {
// logout
}
};
// --------------------------------------------------------------------------------------------
/**
* full module name.
*
* @type {String}
*/
var module_name = this.id;
function new_comico_comics_crawler(configuration, callback, initializer) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
if (typeof initializer === 'function') {
initializer(crawler);
}
// for 年齡確認您是否已滿18歲
crawler.setup_value('cookie', 'islt18age=' + Date.now());
// https://github.com/nodejs/node/issues/27384
// node.js v12 disable TLS v1.0 and v1.1 by default
require('tls').DEFAULT_MIN_VERSION = 'TLSv1';
if (crawler.password && crawler.loginid) {
library_namespace.log([ (crawler.id || module_name) + ': ', {
// gettext_config:{"id":"login-as-$1"}
T : [ 'Login as [%1]', crawler.loginid ]
} ]);
var account_api_host = crawler.base_URL.replace(/^.+?[a-z]+\./,
// https://id.comico.com.tw/login/login.nhn
// https://id.comico.jp/login/login.nhn
'https://id.');
crawler.get_URL(account_api_host + 'login/login.nhn', after_login,
//
{
autoLoginChk : 'Y',
loginid : crawler.loginid,
password : crawler.password,
nexturl : ''
});
} else {
callback(crawler);
}
function after_login(XMLHttp) {
// XMLHttp 只是一個轉址網頁。
// 必須先進入收件箱才能取得所有"有期限的物品"
crawler.get_URL(
// https://id.comico.com.tw/settings/inbox/
account_api_host + 'settings/inbox/', function to_inbox(XMLHttp) {
// 收件箱: 全部接收 有期限的物品
// 受け取りBOX: すべて受け取る
crawler.get_URL(
// https://id.comico.com.tw/api/incentiveall/index.nhn
account_api_host + 'api/incentiveall/index.nhn', get_ticket);
});
}
function get_ticket(XMLHttp) {
// e.g., XMLHttp.responseText ===
// '{"result":[121703625,121703626,121703627,121703628]}'
// console.log(XMLHttp.responseText);
var item_list = JSON.parse(XMLHttp.responseText).result;
if (item_list.length > 0) {
// TODO: 顯示物品的資訊。
library_namespace.info({
// gettext_config:{"id":"$1-items-with-a-time-limit-have-been-received"}
T : [ '已收到 %1 個有期限的{{PLURAL:%1|項目}}。', item_list.length ]
});
}
// 最新消息
// http://www.comico.com.tw/notice/
callback(crawler);
}
}
return new_comico_comics_crawler;
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,317 @@
/**
* @name CeL module for downloading hhcool comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 汗汗酷漫 漫畫 的工具。
*
* <code>
CeL.hhcool(configuration, function(crawler) {
start_crawler(crawler, typeof module === 'object' && module);
}, function(crawler) {
setup_crawler(crawler, typeof module === 'object' && module);
});
</code>
*
* using zh-cmn-Hant-CN .aspx
*
* TODO: http://coco.hhxxee.com/ http://99.hhxxee.com/ http://99770.hhxxee.com/
*
* @since 2019/4/25 5:6:7 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/hhcool.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.hhcool',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
/**
* e.g., <code>
// 汗汗酷漫
<li><a title='野生的最终BOSS出现了' href='/manhua/32449.html'><img src='http://img.94201314.net/comicui/32449.JPG'><br>野生的最终BOSS出现了</a></li>
// 動漫伊甸園
<li><a title='樹鶯吟' target=_blank href='/comicinfo/37621.html'><img src='http://img.94201314.net/comicui/37621.JPG'><br>樹鶯吟</a></li>
</code>
*/
var PATTERN_search = /<li><a title='([^<>'"]+)'[^<>]*? href='\/[^\/]+\/(\d+).html'>.+?<\/li>/g;
/**
* e.g., <code>
<div class='cVolTag'>周刊杂志每周每月连载单集</div><ul class='cVolUl'><li>...</a></li></ul>
<div class='cVolTag'>漫画正片外的剧情之番外篇</div><ul class='cVolUl'><li>...</a></li></ul>
<li><a class='l_s' href='/cool282192/1.html?s=7' target='_blank' title='双星之阴阳师09卷'>双星之阴阳师09卷</a></li>
</code>
*/
// matched: [all, part_title, url, title, inner]
var PATTERN_chapter = /<div class='cVolTag'>([^<>]+)|<li><a [^<>]*?href='([^'<>]+)'[^<>]*? title='([^'<>]+)'[^<>]*>(.+?)<\/a>/g;
var PATTERN_image = /<img (?:.*?) name="([^<>"]+)" (?:.*?)hdNextImg" value="([^<>"]+)"/;
var default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// 本站常常無法取得圖片,因此得多重新檢查。
// 當有多個分部的時候才重新檢查。
recheck : 'multi_parts_changed',
// 當無法取得chapter資料時直接嘗試下一章節。在手動+監視下recheck時可併用此項。
// skip_chapter_data_error : true,
// 當圖像不存在 EOI (end of image) 標記,或是被偵測出非圖像時,依舊強制儲存檔案。
// allow image without EOI (end of image) mark. default:false
allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
// skip_error : true,
// 最小容許圖案檔案大小 (bytes)。
// 對於極少出現錯誤的網站,可以設定一個比較小的數值,並且設定.allow_EOI_error=false。因為這類型的網站要不是無法取得檔案要不就是能夠取得完整的檔案要取得破損檔案並且已通過EOI測試的機會比較少。
// 對於有些圖片只有一條細橫桿的情況。
MIN_LENGTH : 400,
// one_by_one : true,
// base_URL : '',
// /manhua/
base_comic_path : 'manhua',
// 解析 作品名稱 → 作品id get_work()
search_URL : 'comic/?act=search&st=',
parse_search_result : function(html) {
html = html.between('<div class="cComicList">', '</div>');
var id_list = [], id_data = [], matched;
while (matched = PATTERN_search.exec(html)) {
id_list.push(+matched[2]);
id_data.push(matched[1]);
}
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
// e.g., http://www.hhcool.com/manhua/32449.html
return this.base_comic_path + '/' + work_id + '.html';
},
parse_work_data : function(html, get_label, extract_work_data) {
html = html.between('<div id="about_kit">',
'<div class="cVolList">');
html = html.between(null, '<div class="cInfoAct">') || html;
var work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(html.between('<h1>', '</h1>'))
// 選擇性屬性:須配合網站平台更改。
// <meta property="og:novel:status" content="已完结"/>
};
extract_work_data(work_data, html, /<li>([^:]+)(.+?)<\/li>/g);
work_data.status = work_data.状态;
work_data.last_update = work_data.更新;
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
html = html.between('<div class="cVolList">', '<div id="foot">');
work_data.chapter_list = [];
// 漫畫目錄名稱不須包含分部號碼。使章節目錄名稱不包含 part_NO。
// 將會在 function get_chapter_directory_name() 自動設定。
// work_data.chapter_list.add_part_NO = false;
// 轉成由舊至新之順序。
work_data.inverted_order = true;
var matched;
while (matched = PATTERN_chapter.exec(html)) {
// delete matched.input;
// console.log(matched);
if (matched[1]) {
this.set_part(work_data, get_label(matched[1]));
continue;
}
this.add_chapter(work_data, {
title : get_label(matched[3].replace(work_data.title, '')),
url : matched[2]
});
}
// console.log(work_data.chapter_list);
},
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
var html = XMLHttp.responseText;
var chapter_list = [], URL = XMLHttp.responseURL,
// 每一張圖片都得要從載入的頁面獲得資訊。
matched, PATTERN = /csel2\((\d{1,3})\)/g;
while (matched = PATTERN.exec(html)) {
chapter_list.push(matched[1]);
}
work_data.cache_directory = work_data.directory
+ this.cache_directory_name;
library_namespace.create_directory(work_data.cache_directory);
if (!work_data.image_list) {
// image_list[chapter_NO] = [url, url, ...]
work_data.image_list = [];
}
var _this = this,
//
this_image_list = work_data.image_list[chapter_NO] = [];
function for_each_chapter(run_next, NO, index) {
var url = URL.replace(/\/\d{1,3}\.html/, '/' + NO + '.html'),
//
save_to = work_data.cache_directory
+ chapter_NO.pad(work_data.chapter_NO_pad_digits || 3)
+ '-' + NO.pad(work_data.chapter_NO_pad_digits || 3)
+ '.html';
function for_each_image_page(html, error) {
if (error) {
library_namespace.error({
// gettext_config:{"id":"an-error-occurred-while-downloading-and-the-file-contents-could-not-be-obtained-smoothly"}
T : '下載時發生錯誤,無法順利取得檔案內容!'
});
library_namespace.error(error);
_this.onerror(error);
return;
}
var image_data = html.match(PATTERN_image);
// decode chapter image url data
image_data = [ unsuan(image_data[1]), unsuan(image_data[2]) ];
if (image_data[0] !== '\x00') {
if (!this_image_list[index]) {
this_image_list[index] = image_data[0];
} else if (this_image_list[index] !== image_data[0]) {
_this.onerror([ {
// gettext_config:{"id":"different-url-$1-≠-$2"}
T : [ 'Different url: %1 ≠ %2',
//
this_image_list[index], image_data[0] ]
}, '\n', {
// gettext_config:{"id":"maybe-the-downloaded-file-has-an-error?-you-can-try-to-download-it-later-or-use-the-.recheck-option-to-ignore-the-cache-and-re-download-the-page-for-each-image"}
T : '或許是下載的檔案出現錯誤?您可嘗試過段時間再下載,或選用 .recheck 選項來忽略快取、重新下載每個圖片的頁面。'
} ]);
run_next();
return;
}
}
if (image_data[1] !== '\x00') {
this_image_list[index + 1] = image_data[1];
}
// console.log([ index, image_data ])
run_next();
}
// 沒 cache 的話,每一次都要重新取得每個圖片的頁面,速度比較慢。
library_namespace.get_URL_cache(url, for_each_image_page, {
get_URL_options : _this.get_URL_options,
no_write_info : true,
file_name : save_to,
reget : _this.recheck
});
}
chapter_list.run_serial(for_each_chapter, callback);
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var PATTERN = / id="hdDomain"(?:.*?) value="([^<>"]+)"/,
// 不同作品放在不同的location。
matched = html.match(PATTERN);
this.server_list = matched[1].split('|');
var chapter_data = work_data.chapter_list[chapter_NO - 1];
// console.log(work_data.image_list[chapter_NO]);
chapter_data.image_list = work_data.image_list[chapter_NO]
.map(function(url) {
return encodeURI(library_namespace.HTML_to_Unicode(url));
});
return chapter_data;
}
};
function unsuan(s) {
var x = s.substring(s.length - 1);
var w = "abcdefghijklmnopqrstuvwxyz";
var xi = w.indexOf(x) + 1;
var sk = s.substring(s.length - xi - 12, s.length - xi - 1);
s = s.substring(0, s.length - xi - 12);
var k = sk.substring(0, sk.length - 1);
var f = sk.substring(sk.length - 1);
for (var i = 0; i < k.length; i++) {
eval("s=s.replace(/" + k.substring(i, i + 1) + "/g,'" + i + "')");
}
var ss = s.split(f);
s = "";
for (i = 0; i < ss.length; i++) {
s += String.fromCharCode(ss[i]);
}
return s;
}
// --------------------------------------------------------------------------------------------
function new_hhcool_comics_crawler(configuration, callback, initializer) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
if (typeof initializer === 'function') {
initializer(crawler);
}
var decode_filename = 'script/view.js', unsuan;
library_namespace.get_URL_cache(crawler.base_URL + decode_filename,
//
function(contents, error) {
if (false) {
eval('unsuan=function'
+ contents.between('function unsuan', '\nvar'));
}
callback(crawler);
}, crawler.main_directory + decode_filename.match(/[^\\\/]+$/)[0]);
}
return new_hhcool_comics_crawler;
}

View File

@@ -0,0 +1,201 @@
/**
* @name CeL module for downloading jieqi article novels.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見小說管理系統: 杰奇小说连载系统 的工具。
*
* <code>
CeL.jieqi_article(configuration).start(work_id);
</code>
*
* @see http://www.jieqi.com/files/page/html/product/article.html 杰奇网络 杰奇小说连载系统
* 2004-2015?,新版为杰奇原创文学系统)
*
* @since 2019/2/20 16:58:20 模組化。
*/
// More examples:
// @see https://github.com/kanasimi/work_crawler/blob/master/81xsw.js
// @see https://github.com/kanasimi/work_crawler/blob/master/23us.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.jieqi_article',
require : 'application.net.work_crawler.'
//
+ '|application.storage.EPUB.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// auto_create_ebook, automatic create ebook
// MUST includes CeL.application.locale!
need_create_ebook : true,
// recheck:從頭檢測所有作品之所有章節與所有圖片。不會重新擷取圖片。對漫畫應該僅在偶爾需要從頭檢查時開啟此選項。default:false
// recheck='changed': 若是已變更,例如有新的章節,則重新下載/檢查所有章節內容。否則只會自上次下載過的章節接續下載。
recheck : 'changed',
// base_URL : '',
charset : 'gbk',
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
return [ 'modules/article/search.php', {
searchtype : 'articlename',
searchkey : work_title
} ];
},
parse_search_result : function(html, get_label) {
// console.log(html);
var id_list = [], id_data = [];
var matched = html.match(/og:novel:book_name" content="([^<>"]+)"/);
if (matched) {
// 直接進入作品資訊頁面。
id_data.push(get_label(matched[1]));
matched = html
// e.g., <meta property="og:novel:read_url"
// content="https://www.zhuishubang.com/120382/"/>
.match(/og:novel:read_url" content="[^<>"]*?\/(\d+)\/"/);
id_list.push(+matched[1]);
} else {
html.between('<div id="content">', '</table>')
//
.between('<table').each_between('<tr>', '</tr>',
//
function(text) {
var matched = text
// <td class="odd"><a
// href="https://www.kanshushenzhan.com/132800/">万古剑神</a></td>
.match(/ href="[^<>"]+\/(\d+)\/">(.+?)<\/a>/);
id_list.push(+matched[1]);
id_data.push(get_label(matched[2]));
});
}
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return work_id + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
var work_data = {
// 必要屬性:須配合網站平台更改。
// 選擇性屬性:須配合網站平台更改。
// <div class="routeLeft"><a href="/">看书神站</a> > <a
// href="/all/">书库</a>
site_name : get_label(html.between('<div class="routeLeft">',
'</a>'))
};
// overwrite .description
extract_work_data(work_data, html, null, true);
extract_work_data(work_data, html.between('<div class="bookPhr">',
'<div class="renew">'), /<dd>([^]+)(.+)<\/dd>/g);
var matched = html.between('<div class="renew">', '</div>').match(
// <div class="renew">最新章节:<a href="/111269/40729086.html">第535章
// 大结局</a><span>2018-08-01</span></div>
/<a href="([^<>"]+)">(.+?)<\/a>(?:<span>([^<>]+?)<\/span>)?/);
Object.assign(work_data, {
latest_chapter : work_data.latest_chapter_name,
latest_chapter_url : matched[1],
last_update : work_data.update_time || matched[3]
});
// console.log(work_data);
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
work_data.chapter_list = [];
html = html.between('<div class="chapterCon">', '</ul>');
var matched,
// <li><a href="/111269/40724950.html">第1章 甩你一脸</a></li>
PATTERN_chapter = /<a href="([^"<>]+)">([^<>]+)<\/a>/g;
while (matched = PATTERN_chapter.exec(html)) {
// console.log(matched);
var chapter_data = {
url : matched[1],
title : get_label(matched[2])
};
work_data.chapter_list.push(chapter_data);
}
if (this.inverted_order)
work_data.chapter_list.reverse();
// console.log(work_data);
},
// inverted_order : true,
// 取得每一個章節的內容與各個影像資料。 get_chapter_data()
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// 在取得小說章節內容的時候,若發現有章節被目錄漏掉,則將之補上。
this.check_next_chapter(work_data, chapter_NO, html);
// var chapter_data = work_data.chapter_list[chapter_NO - 1];
var sub_title = work_data.previous_sub_title
|| get_label(html.between('<h2>', '</h1>')),
// e.g., https://www.huaxiangju.com/25087/6323179.html
text = html.between('<div class="articleCon">');
text = text.between(null, '<div class="page">')
|| html.between(null, '</div>');
text = text.between('<p>', {
tail : '</p>'
});
if (false && !html.includes('<div class="articleCon">')) {
// console.log(html);
console.log(html.between('<div class="articleCon">', '</div>'));
console.log(text);
}
if (this.remove_ads) {
text = this.remove_ads(text);
}
// console.log(text);
this.add_ebook_chapter(work_data, chapter_NO, {
sub_title : sub_title,
text : text
});
}
};
// --------------------------------------------------------------------------------------------
function new_jieqi_article_novels_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
return new_jieqi_article_novels_crawler;
}

View File

@@ -0,0 +1,410 @@
/**
* @name CeL module for downloading manhuadb comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸漫畫網站 漫画DB 平臺 的工具。
*
* modify from 9mdm.js→dagu.js
*
* 由於 漫画DB 系列網站下載機制較複雜,下載圖片功能為獨立撰寫出來,不支援 achive_images 功能。
*
* <code>
CeL.manhuadb(configuration).start(work_id);
</code>
*
* @since 2021/4/9 19:42:20 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/manhuadb.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/manhuacat.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.manhuadb',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
// manhuadb.js:
// <a class="fixed-a-es" target="_blank" href="/manhua/000/....html"
// title="第01回">第01回</a>
//
// <div class="align-self-center pr-2"><h3 class="h4 mb-0 font-weight-normal
// comic_version_title">[辉夜姬想让人告白~天才们的恋爱头脑战~ 连载]</h3></div>
// manhuacat.js:
// <a class="fixed-a-es"
// href="https://www.manhuacat.com/manga/475/523727.html"
// title="第03话">第03话</a>
//
// <h2 class="h2 mb-0 font-weight-normal comic_version_title">单话</h2>
// [ all, chapter url, title, part title tag name, part title ]
var PATTERN_chapter = /<li[\s\S]+?<a [^<>]*?href="([^<>"]+)"[^<>]*? title="([^<>"]+)"|<(h[23])[^<>]*>(.+?)<\/\3>/g;
var default_configuration = {
// 本站常常無法取得圖片,因此得多重新檢查。
// recheck:從頭檢測所有作品之所有章節與所有圖片。不會重新擷取圖片。對漫畫應該僅在偶爾需要從頭檢查時開啟此選項。
// recheck : true,
// 當有多個分部的時候才重新檢查。
recheck : 'multi_parts_changed',
// 當無法取得chapter資料時直接嘗試下一章節。在手動+監視下recheck時可併用此項。
// skip_chapter_data_error : true,
// allow .jpg without EOI mark.
// allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
// e.g., 736 黄昏流星群/单行本 0005
// [黄昏流星群][弘兼宪史][尖端][volink]Vol_005/736-5-005.jpg
skip_error : true,
// 單行本圖片較多且大,因此採用一個圖一個圖取得的方式。
one_by_one : true,
// 下載圖片的逾時ms數。若逾時時間太小如10秒下載大檔案容易失敗。
timeout : 90 * 1000,
// reget_image_page : true,
// 解析 作品名稱 → 作品id get_work()
search_URL : 'search?q=',
PATTERN_search : /<a href="\/manhua\/(\d+)" title="([^<>"]+)"/,
parse_search_result : function(html, get_label) {
// console.log(html);
var PATTERN_search = this.PATTERN_search;
var id_list = [], id_data = [];
html.each_between('<div class="comicbook-index', '</div>',
function(token) {
// console.log(token);
var matched = token.match(PATTERN_search);
id_list.push(matched[1]);
id_data.push(get_label(matched[2]));
});
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return 'manhua/' + work_id;
},
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
var work_data = {
// 必要屬性:須配合網站平台更改。
// 選擇性屬性:須配合網站平台更改。
// 漫画出版信息
publish : get_label(html.between(
'<div class="comic-pub-data-section', '</div>')
.between('>')),
// 概要 synopsis
description : get_label(html.between(
// manhuadb.js:
'<div class="comic_detail_content">', '</div>')
// manhuacat.js:
|| html.between('<p class="comic_story">', '</p>')
//
.between('漫画简介:'))
};
extract_work_data(work_data, html);
extract_work_data(work_data, html,
/<th scope="row">([^<>]+)<\/th>([\s\S]*?)<\/td>/g);
Object.assign(work_data, {
title : work_data.book_name
});
// console.log(work_data);
return work_data;
},
add_part : true,
get_chapter_list : function(work_data, html, get_label) {
// <div class="comic-toc-section bg-white p-3">
// e.g., 一拳超人
var part_title_list = html.between('<div class="comic-toc-section',
'</ul>').all_between('<li', '</li>').map(function(token) {
return get_label(token.between('>')).replace(/列表$/, '');
});
// console.log(part_title_list);
// <div class="tab-content" id="comic-book-list">
html = html.between(' id="comic-book-list">', '<script ').between(
null, {
tail : '</ol>'
});
var matched, part_NO = 0, part_title, PATTERN_title = new RegExp(
work_data.title + '\\s*'), NO_in_part;
work_data.chapter_list = [];
while (matched = PATTERN_chapter.exec(html)) {
// delete matched.input;
// console.log(matched);
if (matched[4]) {
part_title = get_label(matched[3]).replace(PATTERN_title,
'').replace(/\[\]/g, '');
part_title = part_title_list[part_NO++];
NO_in_part = 0;
continue;
}
++NO_in_part;
var chapter_data = {
// 漫畫目錄名稱不須包含分部號碼。使章節目錄名稱不包含 part_NO。
// part_NO : part_NO,
part_title : part_title,
NO_in_part : NO_in_part,
chapter_NO : NO_in_part,
url : matched[1],
title : get_label(matched[2])
};
work_data.chapter_list.push(chapter_data);
continue;
// ----------------------------------
// 以下: 若是存在舊格式的檔案就把它移成新格式。
// @deprecated
// console.log(chapter_data);
// chapter_data.title = chapter_data.title.replace('文传', '文傳');
var old_directory = work_data.directory
+ work_data.chapter_list.length
// 4: @see chapter_directory_name
// @ CeL.application.net.work_crawler.chapter
.pad(work_data.chapter_NO_pad_digits || 4)
+ ' '
+ (chapter_data.title.includes('[') ? chapter_data.title
: '[' + chapter_data.title + ']'),
//
new_directory = work_data.directory + part_title + ' '
+ NO_in_part.pad(work_data.chapter_NO_pad_digits || 4)
+ ' ' + chapter_data.title;
if (library_namespace.directory_exists(old_directory)) {
library_namespace.move_fso(old_directory, new_directory);
}
var old_archive = old_directory + '.'
+ this.images_archive_extension;
if (library_namespace.file_exists(old_archive)) {
library_namespace.log(old_archive + '\n→ ' + new_directory);
var images_archive = new library_namespace.storage.archive(
old_archive);
images_archive.extract({
cwd : images_archive
});
library_namespace.move_fso(old_directory, new_directory);
library_namespace.remove_file(old_archive);
}
}
work_data.inverted_order = this.inverted_order;
// console.log(work_data.chapter_list);
// console.log(work_data);
},
decode_chapter_data : function(chapter_data) {
return JSON.parse(atob(chapter_data));
},
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
// console.log(XMLHttp);
// console.log(work_data);
// console.log(work_data.chapter_list);
var chapter_data = work_data.chapter_list[chapter_NO - 1],
//
html = XMLHttp.responseText, _this = this, image_page_list = [];
// console.log(html);
chapter_data.title = html.between('<h2 class="h4 text-center">',
'</h2>')
|| chapter_data.title;
var matched = chapter_data.title.match(/^\[([^\[\]]+)\]$/);
if (matched)
chapter_data.title = matched[1];
// --------------------------------------
// 2019/9/17 漫画DB 網站改版
matched = html.between(" img_data = '", "';")
// manhuacat.js
|| html.between(' img_data = "', '"')
// 2019/9/17 5:0
|| html.between('localStorage.setItem("data:"', ');')
.between("'", {
tail : "'"
});
if (matched) {
// console.log(atob(matched));
// console.log(chapter_data);
// 2020/4 漫画DB 網站改版
// @see https://www.manhuadb.com/assets/js/vg-read.js
var image_prefix = this.image_prefix
|| html.between(' data-host="', '"')
+ html.between(' data-img_pre="', '"');
// console.log(image_prefix);
// img_data is base64 encoded, need to do base64 decode before
// json
// decode
chapter_data.image_list = this.decode_chapter_data(matched)
// assert: Array.isArray(chapter_data.image_list);
.map(function(image_data) {
return {
url : encodeURI(image_prefix
//
+ (image_data.img || image_data))
};
});
// console.log(chapter_data.image_list);
callback();
return;
}
// --------------------------------------
html.between('id="page-selector"', '</select>').each_between(
//
'<option value="', '</option>', function(token) {
image_page_list.push({
title : token.between('>'),
url : token.between(null, '"')
});
});
var image_count = image_page_list.length;
// console.log(image_page_list);
if (!(image_count >= 0)) {
throw work_data.title + ' #' + chapter_NO + ' '
+ chapter_data.title + ': Cannot get image count!';
}
// 將過去的 chapter_data.image_list cache 於 work_data.image_list。
if (work_data.image_list) {
chapter_data.image_list = work_data.image_list[chapter_NO - 1];
if (!this.reget_image_page && chapter_data.image_list
&& chapter_data.image_list.length === image_count) {
library_namespace.debug(work_data.title + ' #' + chapter_NO
+ ' ' + chapter_data.title + ': Already got '
+ image_count + ' images.');
chapter_data.image_list = chapter_data.image_list
// .slice() 重建以節省記憶體用量。
.slice().map(function(image_data) {
// 僅保留網址資訊,節省記憶體用量。
return typeof image_data === 'string' ? image_data
// else assert: library_namespace.is_Object(image_data)
: image_data.url;
});
callback();
return;
}
} else {
work_data.image_list = [];
}
function extract_image(XMLHttp) {
var html = XMLHttp.responseText,
//
url = html.between('<img class="img-fluid"', '>').between(
' src="', '"');
library_namespace.debug('Add image '
+ chapter_data.image_list.length + '/' + image_count
+ ': ' + url, 2, 'extract_image');
if (!url && !_this.skip_error) {
_this.onerror('No image url got: #'
+ chapter_data.image_list.length + '/'
+ image_count);
}
// 僅保留網址資訊,節省記憶體用量。
chapter_data.image_list.push(url);
}
chapter_data.image_list = [];
if (image_count > 0)
extract_image(XMLHttp);
library_namespace.run_serial(function(run_next, image_NO, index) {
var image_page_url
//
= _this.full_URL(image_page_list[index - 1].url);
// console.log('Get #' + index + ': ' + image_page_url);
library_namespace.log_temporary('Get image data page of §'
+ chapter_NO + ': ' + image_NO + '/' + image_count);
library_namespace.get_URL(image_page_url, function(XMLHttp) {
extract_image(XMLHttp);
run_next();
}, _this.charset, null, Object.assign({
error_retry : _this.MAX_ERROR_RETRY
}, _this.get_URL_options));
}, image_count, 2, function() {
work_data.image_list[chapter_NO - 1] = chapter_data.image_list
// .slice() 重建以節省記憶體用量。
.slice();
callback();
});
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var chapter_data = work_data.chapter_list[chapter_NO - 1];
// console.log(chapter_data);
// 已在 pre_parse_chapter_data() 設定完 {Array}chapter_data.image_list
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_manhuadb_comics_crawler(configuration, callback, initializer) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
if (typeof initializer === 'function') {
initializer(crawler);
}
if (!crawler.decoder_URL) {
// e.g., comic.cmn-Hans-CN/manhuadb.js
return crawler;
}
// e.g., comic.cmn-Hans-CN/manhuacat.js
library_namespace.get_URL_cache(crawler.decoder_URL, function(contents,
error) {
var LZString;
contents = contents.replace(/var\s+(LZString)/, '$1');
eval(contents);
crawler.LZString = LZString;
callback(crawler);
}, {
directory : crawler.main_directory
});
}
return new_manhuadb_comics_crawler;
}

View File

@@ -0,0 +1,426 @@
/**
* @name CeL module for downloading manhuagui comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸漫畫網站 漫画柜 的工具。
*
* 2017/10: 爱看漫/看漫画改名(DNS被導引到)漫画柜
*
* 57mh 介面程式碼類似於 999comics。manhuagui 似乎是在這基礎上經過修改?
* @see CeL.application.net.work_crawler.sites.SinMH2013
*
* @see http://www.manhua.demo.shenl.com/?theme=mhd
* @see qTcms 晴天漫画程序 晴天漫画系统 http://manhua3.qingtiancms.net/
*
* <code>
CeL.manhuagui(configuration, function(crawler) {
start_crawler(crawler, typeof module === 'object' && module);
}, function(crawler) {
setup_crawler(crawler, typeof module === 'object' && module);
});
</code>
*
* @since 2019/6/17 19:32:13 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/manhuagui.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/manhuagui_tw.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.manhuagui',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
// core_9D227AD5A911B7758A332C9CA35C640C.js
// 2017/3/11 20:6:35: core_33A91659E79CDC4A0F31ED884877F3EF.js
// 2018/1/30與2/2之間改版:
// http://c.3qfm.com/scripts/core_E95EAFDD32D7F97E369526C7AD9A8837.js
// 2018/2/14:
// http://cf.hamreus.com/scripts/core_6B1519CED0A3FA5ED82E8FBDA8F1AB90.js
// 2018/3/16 00:17:25 GMT: add https: for image files
// https://cf.hamreus.com/scripts/core_ABBA2B6ADC1DABE325D505BE3314C273.js
// 2019/6/17 19:47:4
// https://cf.hamreus.com/scripts/core_C0683FDCDEE69940232A703BDEB0F64F.js
// https://cf.hamreus.com/scripts_tw/core_C0683FDCDEE69940232A703BDEB0F64F.js
var core_filename = 'core_C0683FDCDEE69940232A703BDEB0F64F.js',
// https://raw.githubusercontent.com/pieroxy/lz-string/master/libs/lz-string.js
// 2017: main_3A454149B2D2500411BC344B15DB58A4.js'
// 2018/2:
// http://c.3qfm.com/scripts/config_25855B4C08F7A6545A30D049ABD0F9EE.js
// 2018/2/14:
// http://cf.hamreus.com/scripts/config_25855B4C08F7A6545A30D049ABD0F9EE.js
// 2018/3/11 15:59:14 GMT:
// https://cf.hamreus.com/scripts/config_FAF1BF617BAF8A691A828F80672D3588.js
// https://cf.hamreus.com/scripts_tw/config_FAF1BF617BAF8A691A828F80672D3588.js
decode_filename = 'config_FAF1BF617BAF8A691A828F80672D3588.js',
/**
* e.g., <code>
<li><a href="/comic/17515/272218.html" title="第72话一虎进击" class="status0" target="_blank"><span>第72话一…<i>31p</i><em class="new"></em></span></a></li>
https://www.manhuagui.com/comic/4076/
<script type="text/javascript">$.Tabs('#chapter-page-1 li', '#chapter-list-1 ul',{'trigger':'click'});</script><h4><span>单行本</span></h4><div class="chapter-list cf mt10" id='chapter-list-1'><ul style="display:block"><li><a href="/comic/4076/390110.html" title="第67卷" class="status0" target="_blank"><span>第67卷<i>180p</i></span></a></li>
</code>
*
* [:]: incase href="javascript:;"
*
* matched: [ all, href, title, inner, part_title ]
*/
PATTERN_chapter = /<li><a href="([^"<>:]+)" title="([^"<>]+)"[^<>]*>(.+?)<\/a><\/li>|<h4>(.+?)<\/h4>/g;
var default_configuration = {
// 當有多個分部的時候才重新檢查。
recheck : 'multi_parts_changed',
one_by_one : true,
base_URL : 'https://www.manhuagui.com/',
script_base_URL : 'https://cf.hamreus.com/scripts/',
// {Natural}MIN_LENGTH:最小容許圖案檔案大小 (bytes)。
MIN_LENGTH : 400,
// 當網站不允許太過頻繁的訪問/access時可以設定下載之前的等待時間(ms)。
// 模仿實際人工請求。
// 2018/4: manhuagui 不允許過於頻繁的 access會直接 ban IP。
// 2018/7/12 22:29:18: 9s: NG, ban 2 hr.
// 10s, 15s 在下載過100章(1 hr)之後一樣會 ban 5hr。
// 20s, 30s 在下載過200章(~2 hr)之後一樣會 ban。
// 60s 大致OK
// 2019/2/6: 40s: NG, ban 1 day. 50s 在下載過50章後一樣會 ban。.5 day?
// 2019/3/1: 181s: NG. 3~4min 時,似乎會不固定時間檢查、平均每天被封鎖一次,每次封鎖一日?
chapter_time_interval : '4min',
// 2018/3/3 已經不再有常常出現錯誤的情況。
// allow .jpg without EOI mark.
// allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
// skip_error : true,
// 取得伺服器列表。
// use_server_cache : true,
server_URL : function() {
return this.script_base_URL + core_filename;
},
parse_server_list : function(html) {
// console.log(html);
var server_list = [];
eval(html.between('var servs=', ',pfuncs=')).forEach(
function(data) {
data.hosts.forEach(function(server_data) {
// @see SMH.utils.getPath() @ ((core_filename))
server_list.push(server_data.h + '.hamreus.com');
});
});
return server_list;
},
// 解析 作品名稱 → 作品id get_work()
search_URL : '/tools/word.ashx?key=',
parse_search_result : function(html) {
/**
* e.g.,<code>
[ { "t": "西游", "u": "/comic/17515/", "s": false, "cid": 272218, "ct": "第72话一虎进击", "a": "郑健和,邓志辉" } ]
</code>
*/
var id_data = html ? JSON.parse(html) : [];
return [ id_data, id_data ];
},
id_of_search_result : function(search_result) {
// e.g., "/comic/123/"
return +search_result.u.match(/^\/comic\/(\d+)\/$/)[1];
},
title_of_search_result : 't',
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return 'comic/' + work_id + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
var work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(html.between('<h1>', '</h1>')),
// 選擇性屬性:須配合網站平台更改。
status : get_label(html.between('<li class="status">',
'</span>').between('</strong>')),
sub_title : get_label(html.between('<h1>', '</div>').between(
'</h1>')),
description : get_label(html.between('intro-all', '</div>')
.between('>'))
}, data = html.between('detail-list', '</ul>');
extract_work_data(work_data, data,
// e.g., "<strong>漫画别名:</strong>暂无</span>"
/<strong[^<>]*>([^<>]+)<\/strong>(.+?)<\/span>/g);
if (data = get_label(data.between('<li class="status">', '</li>'))) {
library_namespace.log(data);
}
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
var data, chapter_list = [], matched,
//
part_title, part_title_hash = Object.create(null), part_NO = 0;
// 有些尚使用舊模式。
// @see http://www.ikanman.com/comic/8004/
data = html.between('<div class="chapter-bar">',
// <div class="comment mt10" id="Comment">
'class="comment')
// 2017/3/3? ikanman 改版
|| LZString.decompressFromBase64(
//
html.between('id="__VIEWSTATE"', '>').between('value="', '"'));
while (matched = PATTERN_chapter.exec(data)) {
// delete matched.input;
// console.log(matched);
var chapter_data = get_label(matched[4]);
// console.log(chapter_data);
if (chapter_data) {
// console.log(chapter_data);
part_title = chapter_data;
part_title_hash[part_title]
// last part NO. part_NO starts from 1
= ++part_NO;
continue;
}
chapter_data = {
url : matched[1],
title : get_label(matched[2]
// .check_downloaded_chapters() 必須先確保已獲得最終之
// chapter_data.title。
// + ' ' + matched[3].between('<i>', '</i>')
)
};
if (matched = matched[1].match(/(\d+)\.html$/)) {
chapter_data.id = matched[1] | 0;
} else {
chapter_list.some_without_id = chapter_data;
}
if (part_title) {
chapter_data.part_title = part_title
}
chapter_list.push(chapter_data);
}
// console.log(chapter_list);
if (chapter_list.length === 0
// e.g., <div class="book-btn"><a href="/comic/8772/86612.html"
// target="_blank" title="1话" class="btn-read">开始阅读</a>
&& (data = html.between('book-btn', '</a>'))) {
// 尊敬的看漫画用户,应《》版权方的要求,现已删除屏蔽《》漫画所有卷和册,仅保留作品文字简介
this.pre_chapter_URL = this._pre_chapter_URL;
if (Array.isArray(work_data.chapter_list)
&& work_data.chapter_list.length > 1) {
work_data.last_download.chapter
// use cache (old data)
= work_data.chapter_list.length;
} else {
work_data.chapter_list = [ {
url : data.match(/ href="([^<>"]+)"/)[1],
title : data.match(/ title="([^<>"]+)"/)[1]
} ];
}
chapter_list = work_data.chapter_list;
} else {
if (chapter_list.length > 1) {
// 轉成由舊至新之順序。
if (chapter_list.some_without_id) {
library_namespace.warn({
// gettext_config:{"id":"some-chapter-url-names-are-not-numbers-$1"}
T : [ '有些篇章之URL檔名非數字%1',
//
JSON.stringify(chapter_list.some_without_id) ]
});
chapter_list.reverse();
} else {
// 按照章節添加時間排序。
chapter_list.sort(function(a, b) {
// 排序以.html檔案檔名(序號)為準。
// assert: 後來的檔名,序號會比較大。
// @see http://www.ikanman.com/comic/8928/
return a.id - b.id;
});
}
// console.log(chapter_list);
// set latest/max part_NO
chapter_list.part_NO = part_NO;
if (part_NO > 1) {
// rearrange part_NO
// 初始化 NO_in_part
var NO_in_part_hash = new Array(part_NO + 1).fill(0);
chapter_list.forEach(function(chapter_data) {
chapter_data.part_NO
// 在維持分部順序不動的情況下排序 NO_in_part
= part_title_hash[chapter_data.part_title];
chapter_data.NO_in_part
//
= ++NO_in_part_hash[chapter_data.part_NO];
});
// 重新按照 分部→章節順序 排序。
chapter_list.sort(function(a, b) {
// assert: max(NO_in_part) < 1e4
return (a.part_NO - b.part_NO) * 1e4 + a.NO_in_part
- b.NO_in_part;
});
}
}
// console.log(chapter_list);
// console.log(JSON.stringify(chapter_list));
// console.log(chapter_list.slice(0, 20));
// console.log(chapter_list.slice(-20));
// console.log(work_data.chapter_list);
work_data.chapter_list = chapter_list;
if (this.recheck === 'multi_parts_changed'
&& chapter_list.part_NO > 1) {
this.check_downloaded_chapters(work_data, chapter_list);
}
}
},
// 取得每一個章節的各個影像內容資料。 get_chapter_data()
_pre_chapter_URL : function(work_data, chapter_NO, callback) {
var chapter_data = work_data.chapter_list[chapter_NO - 1],
// e.g., "/comic/8772/86612.html"
chapter_id = +chapter_data.url.match(/^\/comic\/\d+\/(\d+)\.html$/)[1];
library_namespace.get_URL(this.base_URL
+ 'support/chapter.ashx?bid=' + work_data.id + '&cid='
+ chapter_id, function(XMLHttp) {
// console.log(XMLHttp.responseText);
chapter_data.sibling = JSON.parse(XMLHttp.responseText);
if (chapter_data.sibling.n > 0
&& work_data.chapter_count === chapter_NO) {
// 還有下一chapter。
work_data.chapter_list.push({
url : chapter_data.url.replace(/(\d+)\.html$/,
chapter_data.sibling.n + '.html')
});
work_data.chapter_count = work_data.chapter_list.length;
}
callback();
}, null, null, this.get_URL_options);
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// decode chapter data
function decode_2016(code) {
code = eval(code);
eval(code.replace('eval', 'code='));
eval(code.replace(/^[^=]+/, 'code'));
return code;
}
// 2017/3/3? ikanman 改版
// String.prototype.splic: used in chapter
function decode(code) {
code = eval(code);
// 2018/3/16 改版
eval(code.between(null, {
tail : ').preInit()'
}).replace('SMH.imgData(', 'code='));
return code;
}
var chapter_data = html.between(
// window["eval"], window["\x65\x76\x61\x6c"]
'<script type="text/javascript">window["\\x65\\x76\\x61\\x6c"]',
'</script>');
if (!chapter_data || !(chapter_data = decode(chapter_data))) {
library_namespace.warn({
// gettext_config:{"id":"unable-to-parse-chapter-data-for-«$1»-§$2"}
T : [ '無法解析《%1》§%2 之章節資料!', work_data.title, chapter_NO ]
});
return;
}
chapter_data = Object.assign(
work_data.chapter_list[chapter_NO - 1], chapter_data);
// for debug
// console.log(chapter_data);
// throw this.id + ': debug throw';
// 設定必要的屬性。
chapter_data.title = chapter_data.cname;
chapter_data.image_count = chapter_data.len;
// e.g., "/ps3/q/qilingu_xmh/第01回上/"
var path = encodeURI(chapter_data.path),
// 令牌 @see SMH.utils.getPicUrl() @ ((core_filename))
token = '?cid=' + chapter_data.cid + '&'
//
+ new URLSearchParams(chapter_data.sl);
// 漫畫櫃的webp圖像檔案可能是即時生成的? 大小常常不一樣。
chapter_data.image_list = chapter_data.files.map(function(url) {
return {
url : path + url + token
}
});
// 當一次下載上百張相片的時候就會被封鎖IP。因此改成一個個下載圖像。
this.one_by_one = chapter_data.image_list.length > 30;
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_manhuagui_comics_crawler(configuration, callback, initializer) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
if (typeof initializer === 'function') {
initializer(crawler);
}
// 創建 main directory。
library_namespace.create_directory(crawler.main_directory);
var LZString;
library_namespace.get_URL_cache(crawler.script_base_URL
+ decode_filename,
// 2017/3/3? ikanman 改版
function(contents, error) {
contents = contents.between('\nwindow["\\x65\\x76\\x61\\x6c"]',
';\n')
//
.replace(/window\[([^\[\]]+)\]/g, function($0, key) {
return eval(key);
});
contents = eval(contents).replace(/^var /, '');
eval(contents);
callback(crawler);
}, crawler.main_directory + decode_filename);
}
return new_manhuagui_comics_crawler;
}

View File

@@ -0,0 +1,206 @@
/**
* @name CeL module for downloading **maybe** qTcms 2014 version comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見漫畫管理系統: **可能為** 舊型晴天漫画CMS (晴天漫画系统 晴天漫画程序,
* NOT 晴天新漫画系统) 的工具。
*
* <code>
CeL.qTcms2014(configuration).start(work_id);
</code>
*
* @see http://manhua.qingtiancms.com/
*
* @since 2019/1/21 模組化。
*/
// More examples:
// @see archive/733dm.201811.js , archive/733dm.201808.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/katui.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.qTcms2014',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// 本站常常無法取得圖片,因此得多重新檢查。
// recheck:從頭檢測所有作品之所有章節與所有圖片。不會重新擷取圖片。對漫畫應該僅在偶爾需要從頭檢查時開啟此選項。
// recheck : true,
// 當無法取得chapter資料時直接嘗試下一章節。在手動+監視下recheck時可併用此項。
// skip_chapter_data_error : true,
// allow .jpg without EOI mark.
// allow_EOI_error : true,
// 當圖像檔案過小,或是被偵測出非圖像(如不具有EOI)時,依舊強制儲存檔案。
// skip_error : true,
// one_by_one : true,
// base_URL : 'http://www.___.net/',
charset : 'gb2312',
// 取得伺服器列表。
// use_server_cache : true,
// katui: /skin/2014mh/global.js
// pufei: /skin/2014mh/global.js?v=41
// taduo: /skin/2014mh/global.js?v=42
server_URL : 'skin/2014mh/global.js',
parse_server_list : function(html) {
var server_list = [],
// e.g., WebimgServerURL[0]="http://img.tsjjx.com/"
// WebimgServerURL[0]="http://www.733mh.com/fd.php?url=http://img.tsjjx.com/";
matched, PATTERN = /\nWebimgServerURL\[\d\]\s*=\s*"([^"]+)"/g;
while (matched = PATTERN.exec(html)) {
server_list.push(matched[1].between('url=') || matched[1]);
}
// console.log(server_list);
return server_list;
},
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
return [ 'e/search/index.php', {
// orderby : 1,
// myorder : 1,
tbname : 'mh',
// tempid:1 @ https://www.dagumanhua.com/
tempid : 3,
show : 'title,player,playadmin,bieming,pinyin',
keyboard : work_title
} ];
},
parse_search_result : function(html) {
// console.log(html);
html = html.between('id="dmList"', '</div>');
var id_list = [], id_data = [];
html.each_between('<li>', '</li>', function(token) {
var matched = token.match(
// pufei.js: <dt><a href="/manhua/32695/index.html"
// title="我靠美食来升级">我靠美食来升级</a></dt>
/<dt><a href="\/(?:mh|manhua)\/(\d+)(?:\/index\.html)?" title="([^"]+)">/
//
);
id_list.push(matched[1]);
id_data.push(matched[2]);
});
// console.log([ id_list, id_data ]);
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return 'manhua/' + work_id + '/';
},
parse_work_data : function(html, get_label) {
var text = html.between('<div class="detailInfo">',
'<div class="intro'),
// work_data={id,title,author,authors,chapters,last_update,last_download:{date,chapter}}
work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(
//
text.between('<div class="titleInfo">', '</h1>')),
// 選擇性屬性:須配合網站平台更改。
status : get_label(text.between('</h1><span>', '</span>')),
description : get_label(html.between(
'<div class="introduction" id="intro1">', '</div>'))
};
text.each_between('<li class="twoCol">', '</li>', function(token) {
work_data[get_label(token.between('<span>', '</span>'))
.replace(/$/, '')] = get_label(token
.between('</span>'));
});
Object.assign(work_data, {
author : work_data.作者,
last_update : work_data.更新时间
});
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
html = html.between('<div id="section">',
'<div class="description">');
work_data.chapter_list = [];
var matched,
// [ , chapter_url, chapter_title ]
PATTERN_chapter = /<a href="(\/manhua\/[^"]+)" title="([^"]+)"/g;
while (matched = PATTERN_chapter.exec(html)) {
work_data.chapter_list.push({
url : matched[1],
title : get_label(matched[2])
});
}
if (work_data.chapter_list.length > 1) {
// 轉成由舊至新之順序。
work_data.chapter_list.reverse();
}
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
function decode(packed) {
var photosr = [];
// decode chapter data @ every picture page
eval(eval(Buffer.from(packed, 'base64').toString().slice(4)));
// 通常[0]===undefined
return photosr.filter(function(url) {
return url;
});
}
var chapter_data = html && html.between('packed="', '"');
if (!chapter_data || !(chapter_data = decode(chapter_data))) {
return;
}
// console.log(JSON.stringify(chapter_data));
// console.log(chapter_data.length);
// library_namespace.set_debug(6);
if (typeof this.postfix_image_url === 'function')
chapter_data = chapter_data.map(this.postfix_image_url);
chapter_data = Object.assign(
// 設定必要的屬性。
work_data.chapter_list[chapter_NO - 1], {
image_list : chapter_data
});
// console.log(JSON.stringify(chapter_data));
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_qTcms2014_comics_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
return new_qTcms2014_comics_crawler;
}

View File

@@ -0,0 +1,553 @@
/**
* @name CeL module for downloading qTcms version 20170501-20190606010315
* comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載中國大陸常見漫畫管理系統: 晴天漫画CMS (晴天漫画系统 晴天漫画程序, 晴天新漫画系统)
* PC端网站 + 手机端网站(行動版 mobile version) 的工具。
*
* <code>
CeL.qTcms2017(configuration).start(work_id);
</code>
*
* modify from 9mdm.js→dagu.js, mh160.js
*
* @see qTcms 晴天漫画程序 晴天漫画系统 http://manhua.qingtiancms.com/
*
* @since 2019/2/3 模組化。
*/
// More examples:
// @see comic.cmn-Hans-CN/nokiacn.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.qTcms2017',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// 因為要經過轉址,所以一個圖一個圖來。
// one_by_one : true,
// base_URL : '',
// fs.readdirSync('.').forEach(function(d){if(/^\d+\s/.test(d))fs.renameSync(d,'manhua-'+d);})
// fs.readdirSync('.').forEach(function(d){if(/^manhua-/.test(d))fs.renameSync(d,d.replace(/^manhua-/,''));})
// 所有作品都使用這種作品類別catalog前綴。
// common_catalog : 'manhua',
// 規範 work id 的正規模式提取出引數中的作品id 以回傳。
extract_work_id : function(work_information) {
if ((this.common_catalog ? /^[a-z\-\d]+$/ : /^[a-z]+_[a-z\-\d]+$/)
.test(work_information))
return work_information;
},
// --------------------------------------
// search comic via web page
// 解析 作品名稱 → 作品id get_work()
search_URL_web : 'statics/search.aspx?key=',
parse_search_result_web : function(html, get_label) {
// console.log(html);
html = html.between('<div class="cy_list">', '</div>');
// console.log(html);
var id_list = [], id_data = [];
html.each_between('<li class="title">', '</li>', function(token) {
// console.log(token);
var matched = token.match(
// [ id, title ]
/<a href="\/([a-z]+\/[a-z\-\d]+)\/"[^<>]*?>([^<>]+)/);
// console.log(matched);
if (this.common_catalog
// 去掉所有不包含作品類別catalog前綴者。
&& !matched[1].startsWith(this.common_catalog + '/'))
return;
id_list.push(this.common_catalog
//
? matched[1].slice((this.common_catalog + '/').length)
// catalog/latin name
: matched[1].replace('/', '_'));
id_data.push(get_label(matched[2]));
}, this);
// console.log([ id_list, id_data ]);
return [ id_list, id_data ];
},
// --------------------------------------
// default: search comic via API
// copy from 360taofu.js
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
return [ 'statics/qingtiancms.ashx', {
cb : 'jQuery' + ('1.7.2' + Math.random()).replace(/\D/g, "")
// @see .expando
+ '_' + Date.now(),
key : work_title,
action : 'GetSear1',
_ : Date.now()
} ];
},
parse_search_result : function(html, get_label) {
// console.log(html);
var data = eval(html.between('(', {
tail : ')'
}));
// console.log(data);
return [ data, data ];
},
id_of_search_result : function(data) {
// console.log(data);
// PC version: .u: webdir + classid1pinyin + titlepinyin + "/"
// webdir: "/"
// classid1pinyin: latin + "/"
// titlepinyin: latin
var matched = data.u
// mobile version
|| data.url;
matched = matched.match(/(?:\/|^)([a-z]+)\/([a-z\-\d]+)\/$/);
// assert: !!matched === true
if (!this.common_catalog)
return matched[1] + '_' + matched[2];
// assert: this.common_catalog === matched[1]
return matched[2];
},
title_of_search_result : 't',
// --------------------------------------
// for mobile version
// 解析 作品名稱 → 作品id get_work()
search_URL_mobile : function(work_title) {
return [ 'statics/qingtiancms.ashx', {
action : 'GetWapSear1',
key : work_title
} ];
},
parse_search_result_mobile : function(html, get_label) {
/**
* @example <code>
{"result": 1000,"msg": "提交成功","data": [{name:'读书成圣',last_update_chapter_name:'014 禁忌十八式',last_updatetime:'',types:'',authors:'',url:'/rexue/dushuchengsheng/'}],"page_data": ""}
</code>
*/
// console.log(JSON.stringify(html));
var data;
try {
eval('data=' + html);
data = data.data;
} catch (e) {
// e.g., "{err!}"
data = [];
}
// console.log(data);
return [ data, data ];
},
title_of_search_result_mobile : 'name',
// --------------------------------------
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return (this.common_catalog ? this.common_catalog + '/' + work_id
// replace only the first '_' to '/'
: work_id.replace('_', '/')) + '/';
},
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
var work_data = html.between('qingtiancms_Details=', ';var');
if (work_data) {
/**
* PC version:
*
* @example <code>
var qingtiancms_Details={G_mubanpage:".html",id:"6638",hits:"9454",webdir:"/",pinglunid:"10",pinglunid1:"",pinglunid2:"cytdbnhsU",pinglunid3:"prod_1368b8102b9177303c660debbbbd257c",title:"读书成圣",classid1pinyin:"rexue/",titlepinyin:"dushuchengsheng"};var uyan_config = {'su':'/6638/'};
</code>
*/
eval('work_data=' + work_data);
} else {
// dagu.js: has NO `qingtiancms_Details`
work_data = Object.create(null);
}
// PC version: nokiacn.js, iqg365.js, 733dm.js
extract_work_data(work_data, html.between(
// <div class="cy_title">\n <h1>相合之物</h1>
'<h1>', ' id="comic-description">'),
/<span>([^<>]+)([\s\S]*?)<\/span>/g);
// PC version: 360taofu.js
extract_work_data(work_data, html.between(
// <div class="mh-date-info fl">\n <div class="mh-date-info-name">
'<div class="mh-date-info', '<div class="work-author">'),
// <span class="one"> 作者: <em>... </span>
// <span> 人气: <em... </span>
// 人气: 收藏数: 吐槽: 状态:
/<span[^<>]*>([^<>]+)([\s\S]*?)<\/span>/g);
// PC version 共通
extract_work_data(work_data, html.between(
// <div class="cy_zhangjie">...<div class="cy_zhangjie_top">
'<div class="cy_zhangjie_top">',
// <div class="cy_plist" id="play_0">
' class="cy_plist"'), /<p>([^<>]+)([\s\S]*?)<\/p>/g);
// PC version, mobile version 共通
extract_work_data(work_data, html);
Object.assign(work_data, this.is_mobile ? {
// 必要屬性:須配合網站平台更改。
last_update : html.between('<span class="date">', '</span>'),
// 選擇性屬性:須配合網站平台更改。
// 網頁中列的description比meta中的完整。
description : get_label(html.between(
// 友绘漫画网
// <p class="txtDesc autoHeight">介绍:...</p>
'<p class="txtDesc autoHeight">', '</p>'))
} : {
// 避免覆寫
qTid : work_data.id,
// 必要屬性:須配合網站平台更改。
title : work_data.title
// nokiacn.js, iqg365.js, 733dm.js
|| get_label(html.between('<h1>', '</h1>')),
author : work_data.作者,
status : work_data.状态,
last_update : work_data.更新时间,
latest_chapter : work_data.最新话,
latest_chapter_url : html.between('最新话:<a href="', '"'),
// 選擇性屬性:須配合網站平台更改。
评分 : work_data.评分 || get_label(html.between(
// 360taofu.js: <p class="fl">评分:<strong class="ui-text-orange"
// id="comicStarDis">...</p>
' id="comicStarDis">', '</p>')),
// 網頁中列的description比meta中的完整。
description : get_label(html.between(
// nokiacn.js, iqg365.js, 733dm.js
// <p id="comic-description">...</p>
' id="comic-description">', '</')) || get_label(html.between(
// 360taofu.js: <div id="workint" class="work-ov">
' id="workint"', '</div>').between('>'))
});
// console.log(work_data);
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
html = html.between('<div class="cy_plist', '</div>')
// mobile version: <div id="list">
// <ul class="Drama autoHeight" id="mh-chapter-list-ol-0">
// 88bag.js: <div id="list" >
|| html.between('<div id="list"', '</ul>');
// console.log(html);
/**
* <code>
76.js: <li><a target="_blank" href="/chuanyue/tangyinzaiyijie/351280.html"><p>杀手唐寅</p><i></i></a></li>
</code>
*/
var matched, PATTERN_chapter =
// matched: [ all, url, inner ]
/<li><a [^<>]*?href="([^<>"]+)"[^<>]*>([\s\S]+?)<\/li>/g;
work_data.chapter_list = [];
while (matched = PATTERN_chapter.exec(html)) {
var chapter_data = {
url : matched[1],
title : get_label(matched[2])
};
work_data.chapter_list.push(chapter_data);
}
// PC version, mobile version 共通
work_data.chapter_list.reverse();
// console.log(work_data.chapter_list);
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// modify from mh160.js
// console.log(html);
var chapter_data = html.between('qTcms_S_m_murl_e="', '"');
// console.log(chapter_data);
if (chapter_data) {
// 對於非utf-8編碼之中文不能使用 atob()
// e.g., http://www.aikanmh.cn/xuanhuan/zhutianji/499631.html
chapter_data = base64_decode(chapter_data)
.split("$qingtiandy$");
}
if (!Array.isArray(chapter_data)) {
library_namespace.warn({
// gettext_config:{"id":"unable-to-parse-chapter-data-for-«$1»-§$2"}
T : [ '無法解析《%1》§%2 之章節資料!', work_data.title, chapter_NO ]
});
return;
}
// console.log(chapter_data);
// console.log(JSON.stringify(chapter_data));
// console.log(chapter_data.length);
// library_namespace.set_debug(6);
// e.g., http://m.88bag.net/rexue/zuomeigongyu/36279.html
// @see
// http://m.88bag.net/template/wap1/css/d7s/js/show.20170501.js?20190722091626
if (chapter_data.length === 1
&& /^(--|\+)https?:\/\//.test(chapter_data[0])) {
chapter_data = {
limited : chapter_data[0].startsWith('+') ? '对不起,该章节已经下架!!本站仅提供检索服务,请尊重作品版权'
: '请点击下方链接开始观看本期漫画:' + chapter_data[0].slice(2)
};
return chapter_data;
}
// 設定必要的屬性。
chapter_data = {
image_list : chapter_data.map(function(url) {
// 2019/10/20: 採用 base64_decode() 取代 atob() 後,
// aikanmh 不可再 encodeURI()。
// url = encodeURI(url);
// 获取当前图片 function f_qTcms_Pic_curUrl_realpic(v)
// http://www.xatxwh.com/template/skin1/css/d7s/js/show.20170501.js?20190117082944
// f_qTcms_Pic_curUrl() → f_qTcms_Pic_curUrl_realpic(v) @
// http://www.nokiacn.net/template/skin2/css/d7s/js/show.20170501.js?20180805095630
if (this.for_each_image) {
// 733dm.js
// for_each_image:function(url,parameters,base64_encode){return(url);}
url = this.for_each_image(url, {
qTcms_S_m_id : html
.between('qTcms_Pic_m_if="', '"'),
qTcms_S_p_id : html.between('qTcms_S_p_id="', '"')
}, base64_encode);
} else if (url.startsWith('/')) {
// e.g., nokiacn.js
var image_base_url = this.image_base_url;
if (!image_base_url && image_base_url !== '') {
// default: url = qTcms_m_weburl + url;
image_base_url = html.between('qTcms_m_weburl="',
'"');
}
url = image_base_url + url;
} else if (html.between('qTcms_Pic_m_if="', '"') !== "2") {
// e.g.,
// http://www.nokiacn.net/lianai/caozuo100/134257.html
url = url.replace(/\?/gi, "a1a1")
.replace(/&/gi, "b1b1").replace(/%/gi, "c1c1");
url = (this.qTcms_m_indexurl
// this.qTcms_m_indexurl: e.g., 517.js
|| html.between('qTcms_m_indexurl="', '"') || '/')
+ "statics/pic/?p="
+ escape(url)
+ "&picid="
+ html.between('qTcms_S_m_id="', '"')
+ "&m_httpurl="
+ escape(base64_decode(html.between(
'qTcms_S_m_mhttpurl="', '"')));
// Should get Status Code: 302 Found
}
return {
url : url
};
}, this)
};
// console.log(JSON.stringify(chapter_data));
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
// http://www.aikanmh.cn/template/skin1/css/d7s/js/show.20170501.js?20191014154954
function utf8_decode(str_data) {
var tmp_arr = [], i = 0, ac = 0, c1 = 0, c2 = 0, c3 = 0;
str_data += '';
while (i < str_data.length) {
c1 = str_data.charCodeAt(i);
if (c1 < 128) {
tmp_arr[ac++] = String.fromCharCode(c1);
i++;
} else if ((c1 > 191) && (c1 < 224)) {
c2 = str_data.charCodeAt(i + 1);
tmp_arr[ac++] = String.fromCharCode(((c1 & 31) << 6)
| (c2 & 63));
i += 2;
} else {
c2 = str_data.charCodeAt(i + 1);
c3 = str_data.charCodeAt(i + 2);
tmp_arr[ac++] = String.fromCharCode(((c1 & 15) << 12)
| ((c2 & 63) << 6) | (c3 & 63));
i += 3;
}
}
return tmp_arr.join('');
}
// 對於非utf-8編碼之中文不能使用 atob()
function base64_decode(data) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, dec = "", tmp_arr = [];
if (!data) {
return data;
}
data += '';
do {
h1 = b64.indexOf(data.charAt(i++));
h2 = b64.indexOf(data.charAt(i++));
h3 = b64.indexOf(data.charAt(i++));
h4 = b64.indexOf(data.charAt(i++));
bits = h1 << 18 | h2 << 12 | h3 << 6 | h4;
o1 = bits >> 16 & 0xff;
o2 = bits >> 8 & 0xff;
o3 = bits & 0xff;
if (h3 == 64) {
tmp_arr[ac++] = String.fromCharCode(o1);
} else if (h4 == 64) {
tmp_arr[ac++] = String.fromCharCode(o1, o2);
} else {
tmp_arr[ac++] = String.fromCharCode(o1, o2, o3);
}
} while (i < data.length);
dec = tmp_arr.join('');
dec = utf8_decode(dec);
return dec;
}
// ------------------------------------------
function utf8_encode(argString) {
var string = (argString + '');
var utftext = "";
var start, end;
var stringl = 0;
start = end = 0;
stringl = string.length;
for (var n = 0; n < stringl; n++) {
var c1 = string.charCodeAt(n);
var enc = null;
if (c1 < 128) {
end++;
} else if (c1 > 127 && c1 < 2048) {
enc = String.fromCharCode((c1 >> 6) | 192)
+ String.fromCharCode((c1 & 63) | 128);
} else {
enc = String.fromCharCode((c1 >> 12) | 224)
+ String.fromCharCode(((c1 >> 6) & 63) | 128)
+ String.fromCharCode((c1 & 63) | 128);
}
if (enc !== null) {
if (end > start) {
utftext += string.substring(start, end);
}
utftext += enc;
start = end = n + 1;
}
}
if (end > start) {
utftext += string.substring(start, string.length);
}
return utftext;
}
// btoa()
function base64_encode(data) {
var b64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=";
var o1, o2, o3, h1, h2, h3, h4, bits, i = 0, ac = 0, enc = "", tmp_arr = [];
if (!data) {
return data;
}
data = utf8_encode(data + '');
do {
o1 = data.charCodeAt(i++);
o2 = data.charCodeAt(i++);
o3 = data.charCodeAt(i++);
bits = o1 << 16 | o2 << 8 | o3;
h1 = bits >> 18 & 0x3f;
h2 = bits >> 12 & 0x3f;
h3 = bits >> 6 & 0x3f;
h4 = bits & 0x3f;
tmp_arr[ac++] = b64.charAt(h1) + b64.charAt(h2) + b64.charAt(h3)
+ b64.charAt(h4);
} while (i < data.length);
enc = tmp_arr.join('');
switch (data.length % 3) {
case 1:
enc = enc.slice(0, -2) + '==';
break;
case 2:
enc = enc.slice(0, -1) + '=';
break;
}
return enc;
}
// --------------------------------------------------------------------------------------------
function new_qTcms2017_comics_crawler(configuration) {
var using_configuration = Object.clone(default_configuration);
if (configuration.using_web_search) {
Object.assign(using_configuration, {
search_URL : using_configuration.search_URL_web,
parse_search_result :
//
using_configuration.parse_search_result_web,
id_of_search_result : null,
title_of_search_result : null
});
} else if (configuration.is_mobile === undefined) {
using_configuration.is_mobile = configuration.base_URL
.includes('://m.');
if (using_configuration.is_mobile) {
Object.assign(using_configuration, {
search_URL : using_configuration.search_URL_mobile,
parse_search_result :
//
using_configuration.parse_search_result_mobile,
title_of_search_result :
//
using_configuration.title_of_search_result_mobile
});
}
}
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(Object.assign(
using_configuration, configuration));
}
return new_qTcms2017_comics_crawler;
}

View File

@@ -0,0 +1,160 @@
/**
* @name CeL module for downloading sequential comics.
*
* @fileoverview 本檔案包含了處理、批量下載 可預測圖片網址序列的漫畫 的工具。
*
* <code>
CeL.work_crawler.sequential(configuration).start(work_id);
</code>
*
* 本檔案為僅僅利用可預測的圖片網址序列去下載漫畫作品,不 fetch 作品與章節頁面的範例。
*
* @since 2019/6/17 21:5:52 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.en-US/mrblue.js
// https://github.com/kanasimi/work_crawler/blob/master/comic.en-US/bookcube.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.sequential',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
one_by_one : true,
// 這類型網站必須靠偵測到錯誤時,轉到下一個章節來運作;因此當圖片下載錯誤時不能直接中斷跳出。
skip_error : true,
// 但是不保留損壞的檔案。
preserve_bad_image : false,
MAX_ERROR_RETRY : 2,
// base_URL : '',
// 規範 work id 的正規模式提取出引數中的作品id 以回傳。
extract_work_id : function(work_information) {
// e.g., "wt_HQ0005"
if (/^[a-z_\-\d]+$/i.test(work_information))
return work_information;
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
// 必須是圖片網址的起始部分。
return '' + work_id + '/';
},
skip_get_work_page : true,
// 解析出作品資料/作品詳情。
parse_work_data : function(html, get_label) {
// 先給一個空的初始化作品資料以便後續作業。
return Object.create(null);
},
// 解析出章節列表。
get_chapter_list : function(work_data, html, get_label) {
if (!Object.hasOwn(this, 'start_chapter_NO')
&& work_data.last_download.chapter > this.start_chapter_NO) {
// 未設定 .start_chapter_NO 且之前下載過,則接續上一次的下載。
this.start_chapter_NO = work_data.last_download.chapter;
}
if (!Array.isArray(work_data.chapter_list)) {
// 先給一個空的章節列表以便後續作業。
work_data.chapter_list = [];
}
// reuse work_data.chapter_list
while (work_data.chapter_list.length < this.start_chapter_NO) {
// 隨便墊入作品資料網址 給本次下載開始下載章節前所有未設定的章節資料,
// 這樣才能準確從 .start_chapter_NO 開始下載。後續章節網址會動態增加。
work_data.chapter_list.push(this.work_URL(work_data.id));
}
// console.log(work_data);
},
// 依照給定序列取得圖片網址。
get_image_url : function(work_data, chapter_NO, image_index) {
return this.work_URL(work_data.id) + chapter_NO + '/'
+ (image_index + 1) + '.jpg';
},
skip_get_chapter_page : true,
// 解析出章節資料。
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
// 設定必要的屬性。
var chapter_data = {
// 先給好本章節第一張圖片的網址。後續圖片網址會動態增加。
image_list : [ this.get_image_url(work_data, chapter_NO, 0) ]
};
// console.log(chapter_data);
return chapter_data;
},
// 設定動態改變章節中的圖片數量。
dynamical_count_images : true,
// 每個圖片下載結束都會執行一次。
after_get_image : function(image_list, work_data, chapter_NO) {
// console.log(image_list);
var latest_image_data = image_list[image_list.index];
// console.log(latest_image_data);
if (!latest_image_data.has_error) {
library_namespace.debug([ work_data.id + ': ', {
// gettext_config:{"id":"the-previous-image-in-this-chapter-was-successfully-downloaded.-download-the-next-image-in-this-chapter"}
T : '本章節上一張圖片下載成功。下載本章節下一幅圖片。'
} ], 3);
image_list.push(this.get_image_url(work_data, chapter_NO,
image_list.length));
return;
}
if (image_list.length === 1) {
library_namespace.debug([ work_data.id + ': ', {
// gettext_config:{"id":"the-first-image-failed-to-download.-ending-download-for-this-work"}
T : '第一張圖就下載失敗了。結束下載本作品。'
} ], 3);
return;
}
// CeL.debug(work_data.id + ': 本章節上一張圖片下載失敗。下載下一個章節的圖片。');
work_data.chapter_list.push(this.work_URL(work_data.id));
// 動態增加章節,必須手動增加章節數量。
work_data.chapter_count++;
}
};
// --------------------------------------------------------------------------------------------
function new_sequential_comics_crawler(configuration, callback, initializer) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
return new library_namespace.work_crawler(configuration);
}
return new_sequential_comics_crawler;
}

View File

@@ -0,0 +1,242 @@
/**
* @name CeL module for downloading Toomics comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 Toomics 韓國漫畫 之 **非韓語版**(外語版本) 的工具。
*
* <code>
CeL.toomics(configuration).start(work_id);
</code>
*
* @since 2019/7/12 20:21:25 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/toomics_sc.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.toomics',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// 所有的子檔案要修訂註解說明時應該都要順便更改在CeL.application.net.comic中Comic_site.prototype內的母comments並以其為主體。
// {Natural}MIN_LENGTH:最小容許圖案檔案大小 (bytes)。
// 對於有些圖片只有一條細橫桿的情況。
MIN_LENGTH : 200,
// 圖像檔案下載失敗處理方式:忽略/跳過圖像錯誤。當404圖像不存在、檔案過小或是被偵測出非圖像(如不具有EOI)時依舊強制儲存檔案。default:false
// skip_error : true,
// one_by_one : true,
base_URL : 'https://toomics.com/',
// LANG_PREFIX : '',
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
return [ this.LANG_PREFIX + '/webtoon/ajax_search', {
toonData : work_title,
offset : 0,
limit : 20
} ];
},
parse_search_result : function(html, get_label) {
html = html.between('<ul>', '</ul>');
// console.log(html);
function parser(token) {
// console.log(token);
return [ +token.match(/toon=(\d+)&/)[1],
/**
* <code>
<a href="/sc/webtoon/search/?toon=4866&return=%2Fsc%2Fwebtoon%2Fepisode%2Ftoon%2F4866">
<div class="search_box">
<p class="img"><img src="https://thumb-g.toomics.com/upload/thumbnail/20180629102024/2019_03_15_15526329389053.jpg" alt="郑主任为何这样"></p>
</code>
*/
token.between('alt="', '"') ];
}
return library_namespace.work_crawler
.extract_work_id_from_search_result_link(
/<li(?:[^<>]*)>([\s\S]+?)<\/li>/g, html, parser);
},
// 取得作品的章節資料。 get_work_data()
work_URL : '/webtoon/episode/toon/',
parse_work_data : function(html, get_label, extract_work_data) {
// console.log(html);
var text = html.between('<header class="ep-cover_ch"', '</header>');
var work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(text.between('<h1>', '</h1>')),
author : get_label(text.between(
/**
* <code>
<span class="writer">李玄敏 <span class="text-muted">|</span> Miyune</span>
</p>
</code>
*/
'<span class="writer">', '</p>')).replace(/ +\|/g, ','),
// 選擇性屬性:須配合網站平台更改。
description : get_label(text.between('<h2>', '</h2>')),
genre : get_label(
text.between('<span class="type">', '</span>'))
.replace(/\s{2,}/g, ' '),
// e.g., Updated every Friday
update_weekday : text.between('<span class="date">', '</span>')
};
extract_work_data(work_data, html);
// console.log(work_data);
return work_data;
},
get_chapter_list : function(work_data, html, get_label) {
html = html.between('<ol class="list-ep">', '</ol>');
var matched, PATTERN_chapter =
//
/<li><a href="([^<>"]+)"[^<>]*>([\s\S]+?)<\/li>/g;
work_data.chapter_list = [];
/**
* <code>
<a href="javascript:;" onclick="Webtoon.chkec(this);location.href='/en/webtoon/detail/code/97236/ep/0/toon/4630'" onkeypress="this.onclick"
data-e="NDYzMA==" data-c="OTcyMzY="
data-v="">
</code>
*/
html.each_between('<li', '</li>', function(token) {
var url = token.between("location.href='", "'");
if (!url) {
// limited
return;
}
work_data.chapter_list.push({
title : get_label(token.between('<div class="cell-num">',
'</div>')),
date : token.between('datetime="', '"'),
type : token
.between('<span class="coin-type1">', '</span>'),
thumb : token.between('data-original="', '"'),
rating : token.between('<span class="star-stat">',
'</span>'),
url : url
});
});
// console.log(work_data.chapter_list);
},
pre_parse_chapter_data
// 執行在解析章節資料 process_chapter_data() 之前的作業 (async)。
// 必須自行保證執行 callback(),不丟出異常、中斷。
: function(XMLHttp, work_data, callback, chapter_NO) {
if (this.verificated) {
callback()
return;
}
this.get_URL(this.LANG_PREFIX
// https://toomics.com/tc/age_verification
// 年齡認證 如需關閉安全模式請確認您已年滿18歲。
+ '/age_verification', function(XMLHttp) {
// console.log(XMLHttp);
this.get_URL(this.LANG_PREFIX
// https://toomics.com/sc/index/set_display/?display=A&return=/sc/webtoon/detail/code/55556/ep/1/toon/2509
+ '/index/set_display/?display=A&return=/' + this.LANG_PREFIX
+ '/help/notice_list', function(XMLHttp) {
// console.log(XMLHttp);
// console.log(this.get_URL_options);
this.verificated = true;
callback();
});
});
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var chapter_data = work_data.chapter_list[chapter_NO - 1];
// console.log(chapter_data);
// console.log(html);
Object.assign(chapter_data, {
title : html.between('<div class="viewer-title">', '</div>')
/**
* <code>
<div class="viewer-title">
<a href="/en/webtoon/episode/toon/4630" title="List">Too Pretty<em>Episode 1</em></a>
</div>
</code>
*/
.between('<em>', '</em>'),
image_list : html
// 2019: '<main class="viewer-body">'
// 2020/8: '<main class="viewer-body viewer-body-scroll">'
.between('<main class="viewer-body', '</main>')
// 2019: .all_between('data-original="', '"')
// 2020/8: .all_between('data-src="', '"')
.all_between('data-src="', '"')
});
if (chapter_data.image_list.length === 0) {
// 改版?
// console.trace(html);
}
// console.log(chapter_data);
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_toomics_comics_crawler(configuration) {
configuration = Object.assign(Object.create(null),
//
default_configuration, {
work_URL : configuration.LANG_PREFIX
+ default_configuration.work_URL
}, configuration);
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
return crawler;
}
return new_toomics_comics_crawler;
}

View File

@@ -0,0 +1,217 @@
/**
* @name CeL module for downloading webtoon comics.
*
* @fileoverview 本檔案包含了解析並處理、批量下載 WEBTOON 韓國漫畫 的工具。
*
* <code>
CeL.webtoon(configuration).start(work_id);
</code>
*
* @see https://www.webtoons.com/ https://github.com/Fastcampus-WPS-9th/Webtoon
*
* @since 2018/7/27 18:16:19 模組化。
*/
// More examples:
// @see
// https://github.com/kanasimi/work_crawler/blob/master/comic.cmn-Hans-CN/dongman.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.webtoon',
require : 'application.net.work_crawler.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
// 2021/12/4 Error: unable to verify the first certificate
//
// https://stackoverflow.com/questions/20082893/unable-to-verify-leaf-signature
// for Error: unable to verify the first certificate
// code: 'UNABLE_TO_VERIFY_LEAF_SIGNATURE'
process.env['NODE_TLS_REJECT_UNAUTHORIZED'] = '0';
var default_configuration = {
// one_by_one : true,
base_URL : 'https://www.webtoons.com/',
// 最小容許圖案檔案大小 (bytes)。
// 對於有些圖片只有一條細橫桿的情況。
// e.g., webtoon\167 吸血鬼也沒關係\0003 [第2話] 健忘症\167-3-036.png
MIN_LENGTH : 130,
// 解析 作品名稱 → 作品id get_work()
search_URL : function(work_title) {
// 預設方法(callback var API)
return 'https://ac.webtoons.com/ac?q='
+ (this.language_code ? this.language_code + '%11' : '')
+ encodeURIComponent(work_title)
+ '&q_enc=UTF-8&st=1&r_lt=0&r_format=json&r_enc=UTF-8&_callback=jQuery'
+ String(Math.floor(Math.random() * 1e10))
+ String(Math.floor(Math.random() * 1e10)) + '_'
+ Date.now() + '&_=' + Date.now();
},
parse_search_result : function(html) {
// console.log(html);
if (html.startsWith('{')) {
html = JSON.parse(html);
} else {
// for callback
html = eval('(' + html.between('(', {
tail : ')'
}) + ')');
}
var id_list = [], id_data = [];
html = html.items[0];
if (html) {
// assert: Array.isArray(html)
html.forEach(function(work_data) {
// console.log(work_data);
if (work_data[1][0] === 'TITLE') {
id_list.push(+work_data[3][0]);
id_data.push(work_data[0][0]);
}
});
}
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
var matched = typeof work_id === 'string'
&& work_id.match(/^([a-z]+)_(\d+)$/);
if (matched) {
// e.g., 投稿新星專區作品
// https://www.webtoons.com/challenge/episodeList?titleNo=
return matched[1] + '/episodeList?titleNo=' + matched[2];
}
return 'episodeList?titleNo=' + work_id;
},
parse_work_data : function(html, get_label, extract_work_data) {
var matched = html
/**
* 咚漫 2018/10/27? 改版 <code>
<a data-buried-obj="1" data-sc-name="PC_detail-page_read-first-btn" href="//www.dongmanmanhua.cn/fantasy/zhexianlu/%E7%AC%AC%E9%9B%B6%E8%AF%9D-1/viewer?title_no=1307&episode_no=1" class="btn_type7 NPI=a:gofirst,g:zh_CN_zh-hans" id="_btnEpisode">阅读第一话<span class="ico_arr21"></span></a>
</code>
*/
.match(/<a [^<>]*?href="([^<>"]+)"[^<>]+id="_btnEpisode">/),
//
text = html.between('<div class="info">', '</div>'),
//
work_data = {
// 必要屬性:須配合網站平台更改。
title : get_label(text.between('<h1 class="subj">', '</h1>')
// https://www.webtoons.com/zh-hant/challenge/%E5%A6%82%E4%BD%A0%E6%89%80%E9%A1%98/list?title_no=166730
|| text.between('<h3 class="subj _challengeTitle">', '<')),
author : get_label(html.between(
// <meta property="com-linewebtoon:webtoon:author"
// content="A / B" />
':webtoon:author" content="', '"')),
// 選擇性屬性:須配合網站平台更改。
// 看第一集, 阅读第一话
chapter_1_url : matched[1],
status : [
get_label(text.between('<h2 class="genre ', '</h2>')
.between('>')),
// 更新頻率 update_frequency
get_label(html.between('<p class="day_info">', '</p>')) ],
description : get_label(html.between(
// ('<p class="summary">', '</p>')
'<meta name="twitter:description" content="', '"')),
last_update : get_label(html.between('<span class="date">',
'</span>'))
};
extract_work_data(work_data, html);
// console.log(work_data);
return work_data;
},
chapter_list_URL : function(work_id, work_data) {
// return url of the first chapter
return work_data.chapter_1_url;
},
get_chapter_list : function(work_data, html) {
// console.log(html);
var data = html.between('<div class="episode_lst">', '</ul>'), matched,
/**
* 咚漫 2018/10/27? 改版 <code>
<a data-sc-event-parameter="{ title_title:'谪仙录',titleNo:'1307',genre:FANTASY,subcategory_"0":DRAMAsubcategory_"1":FANTASY,picAuthor:泼克文化,wriAuthor:泼克文化,update_day:,serial_status:SERIES,reader_gender:男,episode_name:第零话 1,episodeNo:1,change_mode:'',is_read_complete:'',change_episode_direction:''}" data-sc-event-name="TitleReadChangeEpisode" data-buried-obj="1" data-sc-name="PC_read-page_image-episode-btn" href="//www.dongmanmanhua.cn/fantasy/zhexianlu/%E7%AC%AC%E9%9B%B6%E8%AF%9D-1/viewer?title_no=1307&episode_no=1" class="on N=a:vtw.llist,g:zh_CN_zh-hans">
// 穿越時空愚到你 https://www.webtoons.com/zh-hant/drama/2019foolsday/list?title_no=1562
<span class="subj">水下那一分鐘 ft. 奇奇怪怪 <夢境與真實></span>
</code>
*/
PATTERN_chapter = /<li[^<>]*>[\s\S]*?<a [^<>]*?href="([^"<>]+)"[^<>]*>[\s\S]*?<span class="subj">([\s\S]*?)<\/span>[\s\S]*?<\/li>/g;
work_data.chapter_list = [];
while (matched = PATTERN_chapter.exec(data)) {
var chapter_data = {
url : matched[1],
title : matched[2]
};
work_data.chapter_list.push(chapter_data);
}
// console.log(work_data.chapter_list);
},
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var chapter_data = {
// 設定必要的屬性。
title : get_label(html.between(
'<h1 class="subj_episode" title="', '"')),
image_list : []
}, PATTERN_image = /<img [^<>]+?data-url="([^<>"]+)"/g, matched;
html = html.between('<div class="viewer_lst">',
'<div class="episode_area"');
while (matched = PATTERN_image.exec(html)) {
matched = new library_namespace.URI(matched[1]);
// 去掉?type=q70s的部分 畫質較好 q70是手機版 q90是電腦版
delete matched.search_params.type;
// 去除?x-oss-process=image/quality,q_90 可會有更高的畫質
delete matched.search_params['x-oss-process'];
chapter_data.image_list.push({
url : encodeURI(matched.toString())
});
}
return chapter_data;
}
};
// --------------------------------------------------------------------------------------------
function new_webtoon_comics_crawler(configuration) {
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
return crawler;
}
return new_webtoon_comics_crawler;
}

View File

@@ -0,0 +1,304 @@
/**
* @name CeL module for downloading syosetu.com novels.
*
* @fileoverview 本檔案包含了批量下載小説家になろう/小説を読もう!的工具。
*
* TODO: なろう小説API https://dev.syosetu.com/man/api/
*
* <code>
CeL.yomou().start(work_id);
</code>
*
* @see 小説投稿サイト https://matome.naver.jp/odai/2139450042041120001
* http://www.akatsuki-novels.com/novels/ranking_total
* http://www.mai-net.net/bbs/sst/sst.php?act=list&cate=all&page=1
* https://github.com/whiteleaf7/narou https://github.com/59naga/naroujs
* https://github.com/59naga/scrape-narou
*
* @since 2017/2/22 0:18:34 模組化。
*/
// More examples:
// @see https://github.com/kanasimi/work_crawler/blob/master/yomou.js
// @see https://github.com/kanasimi/work_crawler/blob/master/noc.js
'use strict';
// --------------------------------------------------------------------------------------------
// 不採用 if 陳述式,可以避免 Eclipse JSDoc 與 format 多縮排一層。
typeof CeL === 'function' && CeL.run({
// module name
name : 'application.net.work_crawler.sites.yomou',
require : 'application.net.work_crawler.'
//
+ '|application.storage.EPUB.',
// 設定不匯出的子函式。
no_extend : '*',
// 為了方便格式化程式碼,因此將 module 函式主體另外抽出。
code : module_code
});
function module_code(library_namespace) {
// requiring
// --------------------------------------------------------------------------------------------
var default_configuration = {
// auto_create_ebook, automatic create ebook
// MUST includes CeL.application.locale!
need_create_ebook : true,
// recheck:從頭檢測所有作品之所有章節與所有圖片。不會重新擷取圖片。對漫畫應該僅在偶爾需要從頭檢查時開啟此選項。default:false
// recheck='changed': 若是已變更,例如有新的章節,則重新下載/檢查所有章節內容。否則只會自上次下載過的章節接續下載。
recheck : 'changed',
site_name : '小説を読もう!',
base_URL : 'https://yomou.syosetu.com/',
novel_base_URL : 'https://ncode.syosetu.com/',
// 解析 作品名稱 → 作品id get_work()
search_URL : 'search.php?order=hyoka&word=',
parse_search_result : function(html, get_label) {
// console.log(html);
var id_data = [],
// {Array}id_list = [id,id,...]
id_list = [],
// 2019/6/24 目前僅有 ミッドナイトノベルズ 採用這個 header
header = '<article class="search_novel">';
if (!html.includes(header))
header = '<div class="novel_h">';
html.each_between(header, '</a>', function(text) {
id_list.push(text
.between(' href="' + this.novel_base_URL, '/"'));
id_data.push(get_label(text.between('/">')));
}, this);
return [ id_list, id_data ];
},
// 取得作品的章節資料。 get_work_data()
work_URL : function(work_id) {
return this.novel_base_URL + 'novelview/infotop/ncode/' + work_id
+ '/';
},
parse_work_data : function(html, get_label) {
var work_data = Object.create(null);
html.between('<table', '<div id="ad_s_box">')
//
.each_between('<tr>', '</tr>', function(text) {
work_data[get_label(text.between('<th', '</th>').between('>'))]
//
= get_label(text.between('<td', '</td>').between('>'));
});
// console.log(work_data);
work_data = Object.assign({
// 必要屬性:須配合網站平台更改。
title : get_label(html.between('dc:title="', '"')),
// 選擇性屬性:須配合網站平台更改。
// e.g., 连载中, 連載中
// <span id="noveltype">完結済</span>全1部
// <span id="noveltype_notend">連載中</span>全1部
status : [ html.between('<span id="noveltype', '<')
.between('>') ],
author : work_data.作者名,
last_update : work_data.最終話掲載日 || work_data.掲載日,
description : work_data.あらすじ
}, work_data);
// 此兩者為未分割的字串。
if (work_data.ジャンル)
work_data.genre = work_data.ジャンル.split(/\s+/);
if (work_data.キーワード) {
// No キーワード:
// https://novel18.syosetu.com/novelview/infotop/ncode/n3731fh/
work_data.tags = work_data.キーワード.split(/\s+/);
}
return work_data;
},
// 對於章節列表與作品資訊分列不同頁面(URL)的情況,應該另外指定.chapter_list_URL。
chapter_list_URL : function(work_id) {
return this.novel_base_URL + work_id + '/';
},
get_chapter_list : function(work_data, html) {
// TODO: 對於單話,可能無目次。
work_data.chapter_list = [];
html.between('<div class="index_box">', '<div id="novel_footer">')
//
.each_between('<dl class="novel_sublist2">', '</dl>',
//
function(text) {
var matched = text.match(
// [ , href, inner ]
/ href="\/[^\/]+\/([^ "<>]+)[^<>]*>(.+?)<\/a>/);
if (!matched) {
throw text;
}
var chapter_data = {
url : matched[1].replace(/^\.\//, ''),
// 掲載日
date : [ text.match(/>\s*(2\d{3}[年\/][^"<>]+?)</)[1]
//
.to_Date({
zone : work_data.time_zone
}) ],
title : matched[2]
};
if (matched = text.match(/ title="(2\d{3}[年\/][^"<>]+?)改稿"/)) {
chapter_data.date.push(matched[1].to_Date({
zone : work_data.time_zone
}) || matched[1]);
}
work_data.chapter_list.push(chapter_data);
// console.log(chapter_data);
});
if (work_data.chapter_list.length === 0
&& html.includes('<div id="novel_honbun"')) {
// 短編小説
work_data.chapter_list.push({
url : this.chapter_list_URL(work_data.id),
// 掲載日
date : [ work_data.last_update.to_Date({
zone : work_data.time_zone
}) ],
title : work_data.title
});
}
},
// 取得每一個章節的各個影像內容資料。 get_chapter_data()
chapter_URL : function(work_data, chapter_NO) {
var url = work_data.chapter_list[chapter_NO - 1].url;
if (url.includes('://')) {
// e.g., 短編小説
return url;
}
return this.chapter_list_URL(work_data.id) + url;
},
// 檢測所取得內容的章節編號是否相符。
check_chapter_NO : [ '<div id="novel_no">', '/' ],
parse_chapter_data : function(html, work_data, get_label, chapter_NO) {
var
/** {Number}未發現之index。 const: 基本上與程式碼設計合一,僅表示名義,不可更改。(=== -1) */
NOT_FOUND = ''.indexOf('_'),
//
text = html.between('<div id="novel_color">',
'</div><!--novel_color-->'),
//
index = text.indexOf('<div id="novel_p"');
if (index === NOT_FOUND
//
&& (index = text.indexOf('<div id="novel_honbun"')) === NOT_FOUND) {
// gettext_config:{"id":"unable-to-parse-chapter-data-and-get-chapter-content-text"}
throw new Error('無法解析章節資料並取得章節內容文字!');
}
text = text.slice(index);
text = text.between(null, {
// 後半段的"次の話"
tail : '<div class="novel_bn">'
}) || text;
var links = [], ebook = work_data[this.KEY_EBOOK];
text.each_between('<a ', '</a>', function(text) {
var matched = text.match(/(?:^| )href="([^"<>]+)"/);
// @see https://ncode.syosetu.com/n8611bv/49/
// e.g., <a href="https://11578.mitemin.net/i00000/"
if (matched && matched[1].includes('.mitemin.net')) {
// 下載mitemin.net的圖片
links.push(matched[1]);
}
});
links.forEach(function(url) {
// 登記有url正處理中須等待。
ebook.downloading[url] = url;
library_namespace.get_URL(url, function(XMLHttp) {
delete ebook.downloading[url];
if (!XMLHttp || !XMLHttp.responseText) {
return;
}
var matched = XMLHttp.responseText
.match(/<a href="([^"<>]+)" target="_blank">/);
if (matched) {
// 因為.add()會自動添加.downloading並在事後檢查.on_all_downloaded因此這邊不用再檢查。
ebook.add({
url : matched[1]
});
} else {
library_namespace.error({
// gettext_config:{"id":"unable-to-extract-image-url-from-link-in-chapter-content-$1"}
T : [ '無法從章節內容中之連結抽取出圖片網址:%1', url ]
});
}
});
});
var series_title = get_label(
//
html.between('<p class="series_title">', '</p>'));
if (series_title) {
ebook.set([ {
meta : null,
name : "calibre:series",
content : series_title = get_label(series_title)
} ]);
}
this.add_ebook_chapter(work_data, chapter_NO, {
title : html.between('<p class="chapter_title">', '</p>')
// 短編小説
|| series_title,
sub_title : html.between('<p class="novel_subtitle">', '</p>')
// 短編小説
|| html.between('<p class="novel_title">', '</p>'),
text : text
});
}
};
// --------------------------------------------------------------------------------------------
function new_syosetu_crawler(configuration) {
if (configuration && configuration.isR18) {
// 放在這邊,避免覆蓋原設定。
if (!configuration.novel_base_URL)
configuration.novel_base_URL = 'https://novel18.syosetu.com/';
configuration.search_URL = configuration.search_URL
// 解析 作品名稱 → 作品id get_work()
// search/search/search.php hyoka: 総合ポイントの高い順 総合評価の高い順
|| 'search/search/?order=hyoka&word=';
}
configuration = configuration ? Object.assign(Object.create(null),
default_configuration, configuration) : default_configuration;
// 每次呼叫皆創建一個新的實體。
var crawler = new library_namespace.work_crawler(configuration);
if (crawler.isR18) {
// for なろうの関連サイト/R-18サイト 年齢確認
// https://static.syosetu.com/sub/nl/view/js/event/redirect_ageauth.js
crawler.setup_value('cookie', 'over18=yes');
}
if (false) {
crawler.data_of(work_id, function(work_data) {
console.log(work_data);
});
}
return crawler;
}
return new_syosetu_crawler;
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff