cejs
Version:
A JavaScript module framework that is simple to use.
411 lines (355 loc) • 14 kB
JavaScript
/**
* @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
;
// --------------------------------------------------------------------------------------------
// 不採用 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;
}