mirror of
https://scm.univ-tours.fr/22107988t/rappaurio-sae501_502.git
synced 2025-12-17 22:37:30 +01:00
permet l'ajout des frameworks et des routes
This commit is contained in:
3529
app/node_modules/cejs/application/net/Ajax.js
generated
vendored
Normal file
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
187
app/node_modules/cejs/application/net/MIME.js
generated
vendored
Normal 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
388
app/node_modules/cejs/application/net/archive.js
generated
vendored
Normal 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
444
app/node_modules/cejs/application/net/wiki.js
generated
vendored
Normal 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
412
app/node_modules/cejs/application/net/wiki/Flow.js
generated
vendored
Normal 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
75
app/node_modules/cejs/application/net/wiki/README.wiki
generated
vendored
Normal 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
902
app/node_modules/cejs/application/net/wiki/Toolforge.js
generated
vendored
Normal 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
372
app/node_modules/cejs/application/net/wiki/admin.js
generated
vendored
Normal 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 <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> 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 <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> 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 <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> 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 <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> 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 <https://lists.wikimedia.org/mailman/listinfo/mediawiki-api-announce> 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
812
app/node_modules/cejs/application/net/wiki/cache.js
generated
vendored
Normal 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
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
1646
app/node_modules/cejs/application/net/wiki/edit.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
483
app/node_modules/cejs/application/net/wiki/featured_content.js
generated
vendored
Normal file
483
app/node_modules/cejs/application/net/wiki/featured_content.js
generated
vendored
Normal 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
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
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
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
320
app/node_modules/cejs/application/net/wiki/page/Page.js
generated
vendored
Normal 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
2078
app/node_modules/cejs/application/net/wiki/parser.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1567
app/node_modules/cejs/application/net/wiki/parser/evaluate.js
generated
vendored
Normal file
1567
app/node_modules/cejs/application/net/wiki/parser/evaluate.js
generated
vendored
Normal file
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
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
2066
app/node_modules/cejs/application/net/wiki/parser/section.js
generated
vendored
Normal file
2066
app/node_modules/cejs/application/net/wiki/parser/section.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
3971
app/node_modules/cejs/application/net/wiki/parser/wikitext.js
generated
vendored
Normal file
3971
app/node_modules/cejs/application/net/wiki/parser/wikitext.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1109
app/node_modules/cejs/application/net/wiki/query.js
generated
vendored
Normal file
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
4305
app/node_modules/cejs/application/net/wiki/task.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1534
app/node_modules/cejs/application/net/wiki/template_functions.js
generated
vendored
Normal file
1534
app/node_modules/cejs/application/net/wiki/template_functions.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
90
app/node_modules/cejs/application/net/wiki/template_functions/enwiki.js
generated
vendored
Normal file
90
app/node_modules/cejs/application/net/wiki/template_functions/enwiki.js
generated
vendored
Normal 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;
|
||||
}
|
||||
595
app/node_modules/cejs/application/net/wiki/template_functions/general_functions.js
generated
vendored
Normal file
595
app/node_modules/cejs/application/net/wiki/template_functions/general_functions.js
generated
vendored
Normal 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 '{{[[Template:' + parameters[1] + '|' + parameters[1]
|
||||
+ ']]}}';
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
161
app/node_modules/cejs/application/net/wiki/template_functions/jawiki.js
generated
vendored
Normal file
161
app/node_modules/cejs/application/net/wiki/template_functions/jawiki.js
generated
vendored
Normal 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 = ' (' + wikitext + ' ';
|
||||
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;
|
||||
}
|
||||
210
app/node_modules/cejs/application/net/wiki/template_functions/zhmoegirl.js
generated
vendored
Normal file
210
app/node_modules/cejs/application/net/wiki/template_functions/zhmoegirl.js
generated
vendored
Normal 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;
|
||||
}
|
||||
387
app/node_modules/cejs/application/net/wiki/template_functions/zhwiki.js
generated
vendored
Normal file
387
app/node_modules/cejs/application/net/wiki/template_functions/zhwiki.js
generated
vendored
Normal 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;
|
||||
}
|
||||
85
app/node_modules/cejs/application/net/wiki/template_functions/zhwiktionary.js
generated
vendored
Normal file
85
app/node_modules/cejs/application/net/wiki/template_functions/zhwiktionary.js
generated
vendored
Normal 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
745
app/node_modules/cejs/application/net/work_crawler.js
generated
vendored
Normal 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;
|
||||
}
|
||||
51
app/node_modules/cejs/application/net/work_crawler/README.wiki
generated
vendored
Normal file
51
app/node_modules/cejs/application/net/work_crawler/README.wiki
generated
vendored
Normal 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 爬蟲框架]
|
||||
540
app/node_modules/cejs/application/net/work_crawler/arguments.js
generated
vendored
Normal file
540
app/node_modules/cejs/application/net/work_crawler/arguments.js
generated
vendored
Normal 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;
|
||||
}
|
||||
1998
app/node_modules/cejs/application/net/work_crawler/chapter.js
generated
vendored
Normal file
1998
app/node_modules/cejs/application/net/work_crawler/chapter.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1028
app/node_modules/cejs/application/net/work_crawler/ebook.js
generated
vendored
Normal file
1028
app/node_modules/cejs/application/net/work_crawler/ebook.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
665
app/node_modules/cejs/application/net/work_crawler/image.js
generated
vendored
Normal file
665
app/node_modules/cejs/application/net/work_crawler/image.js
generated
vendored
Normal 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;
|
||||
}
|
||||
206
app/node_modules/cejs/application/net/work_crawler/search.js
generated
vendored
Normal file
206
app/node_modules/cejs/application/net/work_crawler/search.js
generated
vendored
Normal 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;
|
||||
}
|
||||
233
app/node_modules/cejs/application/net/work_crawler/sites/AlphaPolis.js
generated
vendored
Normal file
233
app/node_modules/cejs/application/net/work_crawler/sites/AlphaPolis.js
generated
vendored
Normal 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;
|
||||
}
|
||||
375
app/node_modules/cejs/application/net/work_crawler/sites/PTCMS.js
generated
vendored
Normal file
375
app/node_modules/cejs/application/net/work_crawler/sites/PTCMS.js
generated
vendored
Normal 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;
|
||||
}
|
||||
634
app/node_modules/cejs/application/net/work_crawler/sites/SinMH.js
generated
vendored
Normal file
634
app/node_modules/cejs/application/net/work_crawler/sites/SinMH.js
generated
vendored
Normal 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;
|
||||
}
|
||||
288
app/node_modules/cejs/application/net/work_crawler/sites/SinMH2013.js
generated
vendored
Normal file
288
app/node_modules/cejs/application/net/work_crawler/sites/SinMH2013.js
generated
vendored
Normal 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;
|
||||
}
|
||||
168
app/node_modules/cejs/application/net/work_crawler/sites/ace.js
generated
vendored
Normal file
168
app/node_modules/cejs/application/net/work_crawler/sites/ace.js
generated
vendored
Normal 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;
|
||||
}
|
||||
346
app/node_modules/cejs/application/net/work_crawler/sites/baozimh.js
generated
vendored
Normal file
346
app/node_modules/cejs/application/net/work_crawler/sites/baozimh.js
generated
vendored
Normal 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&section_slot=0&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 "&"
|
||||
url : matched[1].replace(/&/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;
|
||||
}
|
||||
744
app/node_modules/cejs/application/net/work_crawler/sites/comico.js
generated
vendored
Normal file
744
app/node_modules/cejs/application/net/work_crawler/sites/comico.js
generated
vendored
Normal 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;
|
||||
// 原先都將標題設在 subtitle,title 沒東西。
|
||||
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;
|
||||
}
|
||||
1102
app/node_modules/cejs/application/net/work_crawler/sites/dm5.js
generated
vendored
Normal file
1102
app/node_modules/cejs/application/net/work_crawler/sites/dm5.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
317
app/node_modules/cejs/application/net/work_crawler/sites/hhcool.js
generated
vendored
Normal file
317
app/node_modules/cejs/application/net/work_crawler/sites/hhcool.js
generated
vendored
Normal 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;
|
||||
}
|
||||
201
app/node_modules/cejs/application/net/work_crawler/sites/jieqi_article.js
generated
vendored
Normal file
201
app/node_modules/cejs/application/net/work_crawler/sites/jieqi_article.js
generated
vendored
Normal 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;
|
||||
}
|
||||
410
app/node_modules/cejs/application/net/work_crawler/sites/manhuadb.js
generated
vendored
Normal file
410
app/node_modules/cejs/application/net/work_crawler/sites/manhuadb.js
generated
vendored
Normal 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;
|
||||
}
|
||||
426
app/node_modules/cejs/application/net/work_crawler/sites/manhuagui.js
generated
vendored
Normal file
426
app/node_modules/cejs/application/net/work_crawler/sites/manhuagui.js
generated
vendored
Normal 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;
|
||||
}
|
||||
206
app/node_modules/cejs/application/net/work_crawler/sites/qTcms2014.js
generated
vendored
Normal file
206
app/node_modules/cejs/application/net/work_crawler/sites/qTcms2014.js
generated
vendored
Normal 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;
|
||||
}
|
||||
553
app/node_modules/cejs/application/net/work_crawler/sites/qTcms2017.js
generated
vendored
Normal file
553
app/node_modules/cejs/application/net/work_crawler/sites/qTcms2017.js
generated
vendored
Normal 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;
|
||||
}
|
||||
160
app/node_modules/cejs/application/net/work_crawler/sites/sequential.js
generated
vendored
Normal file
160
app/node_modules/cejs/application/net/work_crawler/sites/sequential.js
generated
vendored
Normal 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;
|
||||
}
|
||||
242
app/node_modules/cejs/application/net/work_crawler/sites/toomics.js
generated
vendored
Normal file
242
app/node_modules/cejs/application/net/work_crawler/sites/toomics.js
generated
vendored
Normal 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;
|
||||
}
|
||||
217
app/node_modules/cejs/application/net/work_crawler/sites/webtoon.js
generated
vendored
Normal file
217
app/node_modules/cejs/application/net/work_crawler/sites/webtoon.js
generated
vendored
Normal 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;
|
||||
}
|
||||
304
app/node_modules/cejs/application/net/work_crawler/sites/yomou.js
generated
vendored
Normal file
304
app/node_modules/cejs/application/net/work_crawler/sites/yomou.js
generated
vendored
Normal 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;
|
||||
}
|
||||
1235
app/node_modules/cejs/application/net/work_crawler/task.js
generated
vendored
Normal file
1235
app/node_modules/cejs/application/net/work_crawler/task.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
1889
app/node_modules/cejs/application/net/work_crawler/work.js
generated
vendored
Normal file
1889
app/node_modules/cejs/application/net/work_crawler/work.js
generated
vendored
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user