cejs
Version:
A JavaScript module framework that is simple to use.
646 lines (573 loc) • 24 kB
JavaScript
/**
* @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
;
// --------------------------------------------------------------------------------------------
// 不採用 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));
}
// console.trace(chapter_data);
if (!chapter_data.chapterImages.map
&& chapter_data.chapterImages[0]) {
// e.g., https://www.ymh1234.com/comic/20693/1223511.html
// 万古最强宗 88 我踢球的
// {'0':'....jpg','2':...}
chapter_data.chapterImages = Object
.values(chapter_data.chapterImages);
}
// 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;
}