// ==UserScript== // @name For Imhentai // @namespace http://tampermonkey.net/ // @version 2.0 // @description 不翻墙下,更快加载 imhentai.xxx 的图片,并提供打包下载 // @author 水母 // @match https://imhentai.xxx/gallery/* // @match https://*.imhentai.xxx/* // @match https://*.annan-can.top:*/* // @match file:///D:/PROJECT/VSCode/py/* // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw== // @require https://cdn.jsdelivr.net/npm/jszip@3.6.0/dist/jszip.min.js#sha512-uVSVjE7zYsGz4ag0HEzfugJ78oHCI1KhdkivjQro8ABL/PRiEO4ROwvrolYAcZnky0Fl/baWKYilQfWvESliRA== // @grant GM_download // @grant GM_setValue // @grant GM_getValue // @grant GM_openInTab // @license MIT // @downloadURL none // ==/UserScript== // match https://imhentai.xxx/view/* // require file:///D:\PROJECT\VSCode\py\JS script\imhen.js (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; // 图片整体缩放 /** * @desc 标识当前在哪个页面 */ let CURRENT_URL; /** * @enum * @desc 页面枚举 */ const CURRENT_URL_TYPE = { /** * @desc 标识在 https://imhentai.xxx/gallery/? 页面 */ gallery: "gallery", /** * @desc 标识在 https://imhentai.xxx/view/? 页面 */ view: "view", /** * @desc 标识在 https://?.imhentai.xxx/?/?/cover.jpg 页面 */ imgPage: "imgPage", /** * @desc 标识在 测试 gallery 页面 */ testGallery: "testGallery", /** * @desc 标识在 测试 view 页面 */ testView: "testView", /** * @desc 标识为无法识别页面 */ unknow: "unknow", }; // 用户定义的下载页码区间 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); // 图片有效,即加载图片的 base64 // 站点的跨域策略 // canvas 无法加载 gif if ( CURRENT_URL !== CURRENT_URL_TYPE.gallery && CURRENT_URL !== CURRENT_URL_TYPE.testGallery ) { if (type !== ".gif") { try { let c = document.querySelector("#can-cvs"); let ctx = c.getContext("2d"); c.height = ImgObj.naturalHeight; c.width = ImgObj.naturalWidth; ctx.drawImage( ImgObj, 0, 0, ImgObj.naturalWidth, ImgObj.naturalHeight ); imgInfo[keyImageBase64] = c.toDataURL(); } catch (error) { imgInfo[keyImageBase64] = "data:image/png;base64"; console.log(`[Err] ${imgInfo.imgUrl} 处理 base64 error`); } } else { getGifBase64Async(imgInfo); } } imgInfo[keyImgEle] = ImgObj; imgInfo.width = ImgObj.width; imgInfo.height = ImgObj.height; break; } catch (e) { console.log("[Test] 尝试下一个扩展名"); 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 ); }; /** * 获取 Gif 图片的 base64 编码 * @param {ImgInfo} imgInfo */ const getGifBase64Async = async (imgInfo) => { try { let reader = new FileReader(); reader.onloadend = function () { imgInfo[keyImageBase64] = reader.result; // 持续,直至更新计数 let intervalID = setInterval(() => { if (CounterForFileReader.update()) clearInterval(intervalID); }, Math.round(Math.random() * 1000)); }; // 加载图片的 blob 类型数据 if (imgInfo.imgType !== ".err") { let imgBlob = await fetch(imgInfo.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) => { console.log(bzData); 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, }); } document.querySelector(`#${BtnID.downloadBtn}`).innerHTML = "浏览器酱
正在响应"; zip.generateAsync({ type: "blob" }).then((content) => { // saveAs( // content, // `${bzData.name_en} [${UserCustemRange.min}-${UserCustemRange.max}].zip` // ); const downloadUrl = URL.createObjectURL(content); console.log(downloadUrl); GM_download({ url: downloadUrl, name: `${bzData.name_en} [${UserCustemRange.min}-${UserCustemRange.max}].zip`, saveAs: true, onload: () => { // 按钮还原 document.querySelector(`#${BtnID.downloadBtn}`).textContent = BtnText.downloadBtn; document.querySelector(`#${BtnID.downloadBtn}`).disabled = false; IS_DOWNLOADING = false; }, onerror: (error) => { console.log(error); // 按钮还原 document.querySelector(`#${BtnID.downloadBtn}`).textContent = BtnText.downloadBtn; document.querySelector(`#${BtnID.downloadBtn}`).disabled = false; IS_DOWNLOADING = false; }, }); }); }; /** * 数据初始化,获取漫画名、页数、图片的 url */ function initData() { let bzData = new BzData(); let bzDataIterator; console.log(`CURRENT_URL:${CURRENT_URL}`); if ( CURRENT_URL === CURRENT_URL_TYPE.gallery || CURRENT_URL === CURRENT_URL_TYPE.testGallery ) { 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())); // 图片名未编码,数字序列就行 } 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(); // 在 gallary 页面保存数据,跳转翻页 view 页面后使用 // https://m7.imhentai.xxx/024/1mezkq6gp4/cover.jpg let dataKey = coverUrl.substring("https://".length); dataKey = dataKey.substring(0, dataKey.lastIndexOf("/")); // dataKey = "m7.imhentai.xxx/024/1mezkq6gp4" GM_setValue(`BzData:${dataKey}`, bzData); // 测试用 GM_setValue(`BzData:test`, bzData); console.log(`BzData:${dataKey}`); // alert( // JSON.stringify(GM_getValue("BzData:m7.imhentai.xxx/024/1mezkq6gp4")) // ); } else if (CURRENT_URL === CURRENT_URL_TYPE.testView) { bzData = GM_getValue("BzData:test"); bzDataIterator = BzDataIterator(bzData); // 初始化 cover 数据,next() 让 CURRENT_PAGE 与 PAGE_LOADED 能够对齐 let [index, root_url, coverInfo] = bzDataIterator.next().value; processImgInfoAsync(bzData.root_url, coverInfo); } else if (CURRENT_URL === CURRENT_URL_TYPE.imgPage) { let dataKey = window.location.href.substring("https://".length); dataKey = dataKey.substring(0, dataKey.lastIndexOf("/")); bzData = GM_getValue(`BzData:${dataKey}`); bzDataIterator = BzDataIterator(bzData); // 初始化 cover 数据,next() 让 CURRENT_PAGE 与 PAGE_LOADED 能够对齐 let [index, root_url, coverInfo] = bzDataIterator.next().value; processImgInfoAsync(bzData.root_url, coverInfo); } console.log(JSON.stringify(bzData)); console.log(bzData); 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; `; //