// ==UserScript== // @name For Imhentai // @namespace http://tampermonkey.net/ // @version 1.3 // @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'; // Your code here... // 全局数据 let IS_DEBUG = false; 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 IS_FREE_SCALE = false; // 自由缩放 let SCALE = IS_DEBUG ? 0.5 : 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; } }, }; /** * * @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; } /** * * @param {string} imgName * @param {string} imgAlt * @param {string} imgUrl * @param {string} imgType * @param {number} width * @param {number} height * @param {number} SCALE * @param {Image} imgObj * @param {string} imageBase64 */ function ImgInfo( imgName, imgAlt, imgUrl = '', imgType = '', width = 0, height = 0, SCALE = 1, imgObj = 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.imgObj = imgObj; this.imageBase64 = imageBase64; } /** * 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.imgObj = 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; if (IS_DEBUG) console.log(`${index}:`, imgInfo); } } /** * 循环数据,直至所有图片的 imageBase64 加载完全 * @param {BzData} bzData * @param {number} min * @param {number} max */ const watchImgInfoAsync = async (bzData, min, max) => { console.log('watchImgInfoAsync'); document.querySelector(`#${BtnID.downloadBtn}`).textContent = 'Waiting...'; let is_done = false; let intervalID = setInterval( (bzData, is_done) => { if (IS_DEBUG) console.log('watchImgInfoAsync'); // 检查是否加载完全 for (let index = max; index >= min; index--) { if (bzData.imgInfoList[index].imageBase64 === '') break; else is_done = true; } // 更新进度 let percentage = Math.round( (CounterForFileReader.count / (max - min + 1)) * 100 ); if (IS_DEBUG) console.log(`进度: ${percentage}`); 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].imageBase64 !== '') continue; try { let reader = new FileReader(); reader.onloadend = function () { bzData.imgInfoList[i].imageBase64 = reader.result; // 持续,直至更新计数 let intervalID = setInterval(() => { if (CounterForFileReader.update()) clearInterval(intervalID); }, Math.round(Math.random() * 1000)); if (IS_DEBUG) console.log( `[getImageBase64Async:${i}:${ bzData.imgInfoList[i].imgName }].imageBase64: ${bzData.imgInfoList[i].imageBase64.substring( 0, 22 )}...` ); }; // 加载图片的 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 = '打包'; if (IS_DEBUG) console.log('downloadZip'); const zip = new JSZip(); // 图片 url json 文件,去除 imageBase64 数据 let bzDataUser = { ...bzData, imgInfoList: bzData.imgInfoList.map((imgInfo) => { return { ...imgInfo, imageBase64: '' }; }), }; let stringData = JSON.stringify(bzDataUser, null, 2); zip.file(`${bzData.name_en}.json`, stringData); // 图片 zip const fileFolder = zip.folder(bzData.name_en); // 创建 bzData.name_en 文件夹 const fileList = []; for (let i = min; i <= max; i++) { let name = bzData.imgInfoList[i].imgName + bzData.imgInfoList[i].imgType; if (IS_DEBUG) name = `${i}.${name}`; // 使用重复图片,避免文件名相同覆盖 let imageBase64 = bzData.imgInfoList[i].imageBase64.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 + '.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(); if (IS_DEBUG) console.log(coverInfo); 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; `; //