// ==UserScript== // @name For Imhentai // @namespace http://tampermonkey.net/ // @version 1.6 // @description 不翻墙下,更快加载 imhentai.xxx 的图片,并提供打包下载 // @author 水母 // @match https://imhentai.xxx/gallery/* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @grant none // @require https://cdn.bootcdn.net/ajax/libs/jszip/3.10.1/jszip.min.js // @require https://cdn.bootcdn.net/ajax/libs/FileSaver.js/2.0.5/FileSaver.min.js // @license MIT // @downloadURL none // ==/UserScript== (function () { 'use strict'; // 全局数据 let IS_INIT = false; let IS_RUN = false; let IS_DOWNLOADING = false; // 页码序列 [cover.jpg, 1.jpg, ..., 30.jpg] : cover不计入页数; eg.总页数定为 30; 数组 [0] 定为 cover let CURRENT_PAGE = 0; // 当前页码 let MAX_BROWSE_PAGE = 0; // 最大浏览的页码,只增 let PAGE_LOADED = 0; // 已加载的页码 let SCALE = 1; // 图片整体缩放 // 用户定义的下载页码区间 let UserCustemRange = { min: 0, max: 0, page_loaded: 0, }; // enum const BtnID = { runBtn: 'run', previousBtn: 'pre', nextBtn: 'next', downloadBtn: 'down', scaleUpBtn: 'sUp', scaleResetBtn: 'sReset', scaleDownBtn: 'sDown', }; const BtnText = { runBtn: '启动🥰', previousBtn: '⫷', nextBtn: '⫸', downloadBtn: '下载🥵', scaleUpBtn: '⇲', scaleResetBtn: '↺◲', scaleDownBtn: '⇱', }; /** * 异步 FileReader 加载完毕计算器 */ const CounterForFileReader = { is_lock: false, count: 0, /** * 如果更新成功,返回 true * @returns {boolean} */ update() { if (this.is_lock) return false; else { this.is_lock = true; this.count++; this.is_lock = false; return true; } }, }; // 避免这两个没必要下载属性,被 JSON.stringify() const keyImgEle = Symbol('imgEle'); const keyImageBase64 = Symbol('imageBase64'); /** * * @param {string} imgName * @param {string} imgAlt * @param {string} imgUrl * @param {string} imgType * @param {number} width * @param {number} height * @param {number} SCALE * @param {Image} imgEle * @param {string} imageBase64 */ function ImgInfo( imgName, imgAlt, imgUrl = '', imgType = '', width = 0, height = 0, SCALE = 1, imgEle = null, imageBase64 = '' ) { this.imgName = imgName; this.imgAlt = !imgAlt ? imgName : imgAlt; this.imgUrl = imgUrl; this.imgType = imgType; this.width = width; this.height = height; this.SCALE = SCALE; this[keyImgEle] = imgEle; this[keyImageBase64] = imageBase64; } /** * * @param {string} name_en * @param {string} name_sub * @param {number} page * @param {string} root_url * @param {ImgInfo[]} imgInfoList * @param {string[]} types */ function BzData( name_en = 'Null', name_sub = 'Null', page = 0, root_url = '', imgInfoList = [], types = ['.jpg', '.png', '.gif', '.err'] ) { this.name_en = name_en; this.name_sub = name_sub; this.page = page; this.root_url = root_url; this.imgInfoList = imgInfoList; this.types = types; } /** * BzData 迭代器 * @param {BzData} bzData */ function* BzDataIterator(bzData) { let index = 0; while (index < bzData.imgInfoList.length) { let imgInfo = bzData.imgInfoList[index]; yield [index++, bzData.root_url, imgInfo]; } } /** * 漫画名去特殊字符处理 * @param {string} filename 文件名 * @return {string} 处理后的文件名 */ function processFilename(filename) { return filename .replaceAll('\\', '-') .replaceAll('/', '-') .replaceAll(':', ':') .replaceAll('*', '-') .replaceAll('?', '?') .replaceAll('"', '“') .replaceAll('<', '《') .replaceAll('>', '》') .replaceAll('|', '~'); } /** * 判断图片 url 有效与否 * @returns {Promise} */ function verifyImgExists(imgUrl) { return new Promise((resolve, reject) => { let ImgObj = new Image(); ImgObj.src = imgUrl; ImgObj.onload = () => resolve(ImgObj); ImgObj.onerror = (rej) => reject(rej); }); } /** * 为 ImgInfo 测试三种后缀 * @param {string} root_url * @param {ImgInfo} imgInfo * @param {string[]} [types=['.jpg', '.png', '.gif', '.err']] */ async function processImgInfoAsync( root_url, imgInfo, types = ['.jpg', '.png', '.gif', '.err'] ) { // 测试三种后缀 for (let type of types) { imgInfo.imgUrl = root_url + imgInfo.imgName + type; imgInfo.imgType = type; try { let ImgObj = await verifyImgExists(imgInfo.imgUrl); imgInfo[keyImgEle] = ImgObj; imgInfo.width = ImgObj.width; imgInfo.height = ImgObj.height; break; } catch (e) { continue; // 未测试最后一个,继续 } } } /** * 为所有图片生成正确后缀类型 * @param {BzDataIterator} bzDataIterator */ async function processImgAsync(bzDataIterator) { for (let [index, root_url, imgInfo] of bzDataIterator) { await processImgInfoAsync(root_url, imgInfo); PAGE_LOADED = index; } } /** * 循环数据,直至所有图片的 imageBase64 加载完全 * @param {BzData} bzData * @param {number} min * @param {number} max */ const watchImgInfoAsync = async (bzData, min, max) => { document.querySelector(`#${BtnID.downloadBtn}`).textContent = `Loading`; let is_done = false; let intervalID = setInterval( (bzData, is_done) => { // 检查是否加载完全 for (let index = max; index >= min; index--) { if (bzData.imgInfoList[index][keyImageBase64] === '') break; else is_done = true; } // 更新进度 let percentage = Math.round( (CounterForFileReader.count / (max - min + 1)) * 100 ); document.querySelector( `#${BtnID.downloadBtn}` ).textContent = `Loading ${percentage}%`; if (is_done) { // 下载开始 clearInterval(intervalID); downloadZip(bzData, min, max); } }, 1500, bzData, is_done ); }; /** * 获取图片的 base64 编码 * @param {BzData} bzData * @param {number} min * @param {number} max */ const getImageBase64Async = async (bzData, min, max) => { for (let i = min; i <= max; i++) { if (bzData.imgInfoList[i][keyImageBase64] !== '') continue; try { let reader = new FileReader(); reader.onloadend = function () { bzData.imgInfoList[i][keyImageBase64] = reader.result; // 持续,直至更新计数 let intervalID = setInterval(() => { if (CounterForFileReader.update()) clearInterval(intervalID); }, Math.round(Math.random() * 1000)); }; // 加载图片的 blob 类型数据 if (bzData.imgInfoList[i].imgType !== '.err') { let imgBlob = await fetch(bzData.imgInfoList[i].imgUrl).then( (respone) => respone.blob() ); reader.readAsDataURL(imgBlob); // 将 blob 数据转换成 DataURL 数据 } else { reader.readAsDataURL(new Blob()); // 空文件 } } catch (e) { console.error(e); } } }; /** * 批量下载图片 * @param {BzData} bzData 图像数据 * @param {number} min * @param {number} max */ const downloadZip = async (bzData, min, max) => { document.querySelector(`#${BtnID.downloadBtn}`).textContent = '打包'; const zip = new JSZip(); // 图片 url json 文件 let stringData = JSON.stringify(bzData, null, 2); zip.file(`${bzData.name_en} [${UserCustemRange.min}-${UserCustemRange.max}].json`, stringData); // 图片 zip const fileFolder = zip.folder(`${bzData.name_en} [${UserCustemRange.min}-${UserCustemRange.max}]`); // 创建 bzData.name_en 文件夹 const fileList = []; for (let i = min; i <= max; i++) { let name = bzData.imgInfoList[i].imgName + bzData.imgInfoList[i].imgType; let imageBase64 = bzData.imgInfoList[i][keyImageBase64].substring(22); // 截取 data:image/png;base64, 后的数据 fileList.push({ name: name, img: imageBase64 }); } // 往 zip 中,添加每张图片数据 for (let imgFile of fileList) { fileFolder.file(imgFile.name, imgFile.img, { base64: true, }); } zip.generateAsync({ type: 'blob' }).then((content) => { saveAs(content, `${bzData.name_en} [${UserCustemRange.min}-${UserCustemRange.max}].zip`); }); // 按钮还原 document.querySelector(`#${BtnID.downloadBtn}`).textContent = BtnText.downloadBtn; IS_DOWNLOADING = false; }; /** * 数据初始化,获取漫画名、页数、图片的 url */ function initData() { let bzData = new BzData(); let coverUrl; // cover bzData.imgInfoList.push(new ImgInfo('cover')); const tag_div_main = document.querySelectorAll( 'body > div.overlay > div.container > div.row.gallery_first > div' ); // 获取漫画名 bzData.name_en = tag_div_main[1].querySelector('h1').textContent; bzData.name_sub = tag_div_main[1].querySelector('p.subtitle').textContent; // 漫画名去特殊字符处理 if (bzData.name_sub !== '') { bzData.name_sub = processFilename(bzData.name_sub); } if (bzData.name_en !== '') { bzData.name_en = processFilename(bzData.name_en); } else { bzData.name_en = bzData.name_sub; } // 获取页数 let page_str = tag_div_main[1].querySelector('li.pages').textContent; bzData.page = Number.parseInt(page_str.match(/Pages: ([0-9]*)/i)[1]); // 图片序列的 url 前缀与封面 url 的相同, // eg.封面 url=https://m7.imhentai.xxx/023/mnsiote3jg/cover.jpg // eg.序列的 url=https://m7.imhentai.xxx/023/mnsiote3jg/ coverUrl = tag_div_main[0].querySelector('img').dataset.src; bzData.root_url = coverUrl.slice(0, coverUrl.lastIndexOf('/') + 1); // 图片序列的 url 生成, // eg: https://m6.imhentai.xxx/021/fh5n1d304g/1.jpg for (let p = 1; p <= bzData.page; p++) { bzData.imgInfoList.push(new ImgInfo(p.toString())); // 图片名未编码,数字序列就行 } let bzDataIterator = BzDataIterator(bzData); // 初始化 cover 数据,让 CURRENT_PAGE 与 PAGE_LOADED 能够对齐 let [index, root_url, coverInfo] = bzDataIterator.next().value; let ImgObj = new Image(); ImgObj.src = coverUrl; ImgObj.onload = () => { coverInfo.width = ImgObj.width; coverInfo.height = ImgObj.height; }; coverInfo.imgUrl = coverUrl; coverInfo.imgType = coverUrl .substring(coverUrl.lastIndexOf('.')) .toLowerCase(); return [bzData, bzDataIterator]; } /** * 初始化组件 * @param {BzData} bzData * @param {BzDataIterator} bzDataIterator */ function initComponents(bzData, bzDataIterator) { // const newImg = document.createElement('img'); newImg.id = 'can-img'; newImg.style = ` -webkit-user-select: none; margin:0 auto; transition: background-color 300ms; `; // const changePageInput = document.createElement('input'); changePageInput.id = 'can-input'; changePageInput.type = 'number'; changePageInput.value = `${CURRENT_PAGE}`; changePageInput.disabled = true; changePageInput.style = ` width: 45%;height: 80%; font-size:18px;text-align:center; `; //