您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Jav-鉴黄师 收藏、屏蔽、标记已下载; 免VIP查看热榜、Top250排行榜、Fc2ppv等数据; 可查看所有评论信息; 支持云盘备份; 以图识图
当前为
// ==UserScript== // @name JAV-JSH // @namespace https://sleazyfork.org/zh-CN/scripts/533695-jav-jhs // @version 1.6.7 // @author xie bro // @description Jav-鉴黄师 收藏、屏蔽、标记已下载; 免VIP查看热榜、Top250排行榜、Fc2ppv等数据; 可查看所有评论信息; 支持云盘备份; 以图识图 // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=javdb.com // @include https://javdb*.com/* // @include https://www.javbus.com/* // @include https://javtrailers.com/* // @include https://subtitlecat.com/* // @include https://www.aliyundrive.com/* // @exclude https://www.javbus.com/forum/* // @exclude https://www.javbus.com/*actresses // @require data:application/javascript,;(function%20hookBody()%20%7B%20if%20(document.readyState%20!%3D%3D%20%22loading%22)%20%7B%20return%3B%20%7D%20const%20initialHideStyle%20%3D%20document.createElement(%22style%22)%3B%20initialHideStyle.textContent%20%3D%20%60%20body%20%7B%20opacity%3A%200%20!important%3B%20visibility%3A%20hidden%20!important%3B%20%7D%20body.script-ready%20%7B%20opacity%3A%201%20!important%3B%20visibility%3A%20visible%20!important%3B%20%7D%20%60%3B%20document.head.appendChild(initialHideStyle)%3B%20setTimeout(()%20%3D%3E%20%7B%20document.body.classList.add(%22script-ready%22)%3B%20%7D%2C%203e3)%3B%20%7D)()%3B // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/layer.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/js/md5.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.js // @require https://cdn.jsdelivr.net/npm/[email protected]/dist/localforage.min.js // @connect hohoj.tv // @connect xunlei.com // @connect geilijiasu.com // @connect aliyundrive.com // @connect aliyundrive.net // @connect ja.wikipedia.org // @connect beta.magnet.pics // @connect jdforrepam.com // @connect * // @grant GM_xmlhttpRequest // @grant GM_download // @run-at document-start // ==/UserScript== var __defProp = Object.defineProperty, __typeError = msg => { throw TypeError(msg); }, __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: !0, configurable: !0, writable: !0, value: value }) : obj[key] = value, __publicField = (obj, key, value) => __defNormalProp(obj, "symbol" != typeof key ? key + "" : key, value), __accessCheck = (obj, member, msg) => member.has(obj) || __typeError("Cannot " + msg), __privateAdd = (obj, member, value) => member.has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value), __privateMethod = (obj, member, method) => (__accessCheck(obj, member, "access private method"), method); !function() { "use strict"; var _StorageManager_instances, autoCleanup_fn, saveFilterItem_fn, _SettingPlugin_instances, handleSyncData_fn; const Bus = { boxSelector: ".masonry", itemSelector: ".masonry .item", coverImgSelector: ".item .photo-frame img", requestDomItemSelector: "#waterfall .item" }, Db = { boxSelector: ".movie-list", itemSelector: ".movie-list .item", coverImgSelector: ".cover img", requestDomItemSelector: ".movie-list .item" }, isJavDb = window.location.href.includes("javdb"), isJavBus = window.location.href.includes("javbus"), Status_FAVORITE = "favorite", Status_FILTER = "filter", Status_HAS_DOWN = "hasDown"; function insertStyle(css) { if (css) if (css.includes("<style>")) document.head.insertAdjacentHTML("beforeend", css); else { const style = document.createElement("style"); style.textContent = css; document.head.appendChild(style); } } isJavBus && insertStyle("\n<style>\n .masonry {\n height: 100% !important;\n width: 100% !important;\n padding: 0 15px !important;\n }\n .masonry {\n display: grid;\n column-gap: 10px; /* 列间距*/\n row-gap: 10px; /* 行间距 */\n grid-template-columns: repeat(4, minmax(0, 1fr));\n }\n .masonry .item {\n /*position: initial !important;*/\n top: initial !important;\n left: initial !important;\n float: none !important;\n background-color:#c4b1b1;\n position: relative !important;\n }\n \n .masonry .item:hover {\n box-shadow: 0 .5em 1em -.125em rgba(10, 10, 10, .1), 0 0 0 1px #485fc7;\n }\n .masonry .movie-box{\n width: 100% !important;\n height: 100% !important;\n margin: 0 !important;\n }\n .masonry .movie-box .photo-frame {\n height: 70% !important;\n margin: 0 !important;\n }\n .masonry .movie-box img {\n max-height: 300px;\n height: 100% !important;\n object-fit: cover; /* 保持比例,裁剪多余部分 */\n object-position: top; /* 从中间裁剪(可调整:top, bottom, left, right) */\n }\n .masonry .movie-box img:hover {\n transform: scale(1.04);\n transition: transform 0.3s;\n }\n .masonry .photo-info{\n height: 30% !important;\n }\n .masonry .photo-info span {\n display: inline-block; /* 或者 block */\n max-width: 100%; /* 根据父容器限制宽度 */\n white-space: nowrap; /* 禁止换行 */\n overflow: hidden; /* 隐藏溢出内容 */\n text-overflow: ellipsis; /* 显示省略号 */\n }\n \n /* 无码页面的样式 */\n .photo-frame .mheyzo,\n .photo-frame .mcaribbeancom2{\n margin-left: 0 !important;\n }\n .avatar-box{\n width: 100% !important;\n display: flex !important;\n margin:0 !important;\n }\n .avatar-box .photo-info{\n display: flex;\n justify-content: center;\n align-items: center;\n gap: 30px;\n flex-direction: row;\n background-color:#fff !important;\n }\n /*.photo-info .item-tag{\n position: relative;\n }*/\n footer,#related-waterfall{\n display: none!important;\n }\n</style>\n"); isJavDb && insertStyle('\n<style>\n .navbar {\n z-index: 12345679 !important;\n }\n \n .sub-header,\n #search-bar-container, /*搜索框*/\n #footer,\n .search-recent-keywords, /*搜索框底部热搜词条*/\n .app-desktop-banner,\n div[data-controller="movie-tab"] .tabs,\n h3.main-title,\n div.video-meta-panel > div > div:nth-child(2) > nav > div.review-buttons > div:nth-child(2), /* 下载 订正 按钮*/\n div.video-detail > div:nth-child(4) > div > div.tabs.no-bottom > ul > li:nth-child(3), /* 相关清单*/\n div.video-detail > div:nth-child(4) > div > div.tabs.no-bottom > ul > li:nth-child(2), /* 短评按钮*/\n div.video-detail > div:nth-child(4) > div > div.tabs.no-bottom > ul > li:nth-child(1), /*磁力面板 按钮*/\n .top-meta,\n .float-buttons {\n display: none !important;\n }\n \n div.tabs.no-bottom,\n .tabs ul {\n border-bottom: none !important;\n }\n \n \n /* 视频列表项 相对相对 方便标签绝对定位*/\n .movie-list .item {\n position: relative !important;\n }\n\n</style>\n'); insertStyle("\n<style>\n .a-primary, /* 主按钮 - 浅蓝色 */\n .a-success, /* 成功按钮 - 浅绿色 */\n .a-danger, /* 危险按钮 - 浅粉色 */\n .a-warning, /* 警告按钮 - 浅橙色 */\n .a-info, /* 信息按钮 - 浅青色 */\n .a-dark, /* 深色按钮 - 改为中等灰色(保持浅色系中的对比) */\n .a-outline, /* 轮廓按钮 - 浅灰色边框 */\n .a-disabled /* 禁用按钮 - 极浅灰色 */\n {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 6px 14px;\n margin-left: 10px;\n border-radius: 6px;\n text-decoration: none;\n font-size: 13px;\n font-weight: 500;\n transition: all 0.2s ease;\n cursor: pointer;\n border: 1px solid rgba(0, 0, 0, 0.08);\n white-space: nowrap;\n }\n \n .a-primary {\n background: #e0f2fe;\n color: #0369a1;\n border-color: #bae6fd;\n }\n \n .a-primary:hover {\n background: #bae6fd;\n }\n \n .a-success {\n background: #dcfce7;\n color: #166534;\n border-color: #bbf7d0;\n }\n \n .a-success:hover {\n background: #bbf7d0;\n }\n \n .a-danger {\n background: #fee2e2;\n color: #b91c1c;\n border-color: #fecaca;\n }\n \n .a-danger:hover {\n background: #fecaca;\n }\n \n .a-warning {\n background: #ffedd5;\n color: #9a3412;\n border-color: #fed7aa;\n }\n \n .a-warning:hover {\n background: #fed7aa;\n }\n \n .a-info {\n background: #ccfbf1;\n color: #0d9488;\n border-color: #99f6e4;\n }\n \n .a-info:hover {\n background: #99f6e4;\n }\n \n .a-dark {\n background: #e2e8f0;\n color: #334155;\n border-color: #cbd5e1;\n }\n \n .a-dark:hover {\n background: #cbd5e1;\n }\n \n .a-outline {\n background: transparent;\n color: #64748b;\n border-color: #cbd5e1;\n }\n \n .a-outline:hover {\n background: #f8fafc;\n }\n \n .a-disabled {\n background: #f1f5f9;\n color: #94a3b8;\n border-color: #e2e8f0;\n cursor: not-allowed;\n }\n \n .a-disabled:hover {\n transform: none;\n box-shadow: none;\n background: #f1f5f9;\n }\n</style>\n"); insertStyle("\n<style>\n /* 全局通用样式 */\n .fr-btn {\n float: right;\n margin-left: 4px !important;\n }\n \n .menu-box {\n position: fixed;\n right: 10px;\n top: 50%;\n transform: translateY(-50%);\n display: flex;\n flex-direction: column;\n z-index: 1000;\n gap: 6px;\n }\n \n .menu-btn {\n display: inline-block !important;\n min-width: 80px;\n padding: 7px 12px;\n border-radius: 4px;\n color: white !important;\n text-decoration: none;\n font-weight: bold;\n font-size: 12px;\n text-align: center;\n cursor: pointer;\n transition: all 0.3s ease;\n box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);\n text-shadow: 0 1px 1px rgba(0, 0, 0, 0.2);\n border: none;\n line-height: 1.3;\n margin: 0;\n }\n \n .menu-btn:hover {\n transform: translateY(-1px);\n box-shadow: 0 3px 6px rgba(0, 0, 0, 0.15);\n opacity: 0.9;\n }\n \n .menu-btn:active {\n transform: translateY(0);\n box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);\n }\n \n .do-hide {\n display: none !important;\n }\n</style>\n"); _StorageManager_instances = new WeakSet; autoCleanup_fn = async function() { if (!window.location.hostname.includes("javdb")) return; (await this.forage.keys()).forEach((k => k.startsWith("SCORE_") && this.forage.removeItem(k))); const now = Date.now(); try { const lastCleanupTime = await this.forage.getItem("lastCleanupTime"); if (lastCleanupTime && now - lastCleanupTime < 864e5) return; const keys = await this.forage.keys(); for (const key of keys) { if (this.interceptedKeys.includes(key)) continue; const storedData = await this.forage.getItem(key); if ("object" == typeof storedData && "expires" in storedData && "expiresStr" in storedData && Date.now() > storedData.expires) { console.log("清理过期数据:", key); await this.forage.removeItem(key); } } await this.forage.setItem("lastCleanupTime", now); } catch (error) { console.error("[自动清理失败]", error); await this.forage.setItem("lastCleanupTime", now); } }; saveFilterItem_fn = async function(items, storageKey, itemName) { let itemList; if (Array.isArray(items)) itemList = [ ...items ]; else { itemList = await this.forage.getItem(storageKey) || []; if (itemList.includes(items)) { const errorMsg = `${items} ${itemName}已存在`; show.error(errorMsg); throw new Error(errorMsg); } itemList.push(items); } await this.forage.setItem(storageKey, itemList); return itemList; }; let StorageManager = class _StorageManager { constructor() { __privateAdd(this, _StorageManager_instances); __publicField(this, "car_list_key", "car_list"); __publicField(this, "filter_actor_key", "filter_actor"); __publicField(this, "title_filter_keyword_key", "title_filter_keyword"); __publicField(this, "review_filter_keyword_key", "review_filter_keyword"); __publicField(this, "setting_key", "setting"); __publicField(this, "auto_page_key", "autoPage"); __publicField(this, "fold_category_key", "foldCategory"); __publicField(this, "review_ts_key", "review_ts"); __publicField(this, "review_sign_key", "review_sign"); __publicField(this, "actress_prefix_key", "z_actress_"); __publicField(this, "score_prefix_key", "z_score_"); __publicField(this, "forage", localforage.createInstance({ driver: localforage.INDEXEDDB, name: "JAV-JSH", version: 1, storeName: "appData" })); __publicField(this, "interceptedKeys", [ this.car_list_key, this.filter_actor_key, this.title_filter_keyword_key, this.review_filter_keyword_key, this.setting_key ]); if (_StorageManager.instance) throw new Error("LocalStorageManager已被实例化过了!"); _StorageManager.instance = this; __privateMethod(this, _StorageManager_instances, autoCleanup_fn).call(this).then(); } async saveFilterActor(keywords) { return __privateMethod(this, _StorageManager_instances, saveFilterItem_fn).call(this, keywords, this.filter_actor_key, "演员"); } async saveReviewFilterKeyword(keywords) { return __privateMethod(this, _StorageManager_instances, saveFilterItem_fn).call(this, keywords, this.review_filter_keyword_key, "评论关键词"); } async saveTitleFilterKeyword(keywords) { return __privateMethod(this, _StorageManager_instances, saveFilterItem_fn).call(this, keywords, this.title_filter_keyword_key, "标题关键词"); } async getFilterActorList() { return await this.forage.getItem(this.filter_actor_key) || []; } async getTitleFilterKeyword() { return await this.forage.getItem(this.title_filter_keyword_key) || []; } async getSetting(attribute = null, defaultVal) { const settingObj = await this.forage.getItem(this.setting_key) || {}; if (null === attribute) return settingObj; const value = settingObj[attribute]; return value ? "true" === value || "false" === value ? "true" === value.toLowerCase() : "string" != typeof value || isNaN(Number(value)) ? value : Number(value) : defaultVal; } async saveSetting(settingObj) { await this.forage.setItem(this.setting_key, settingObj); } async getReviewFilterKeywordList() { return await this.forage.getItem(this.review_filter_keyword_key) || []; } async saveCar(carNum, url, actress, actionType) { if (!carNum) { show.error("番号为空!"); throw new Error("番号为空!"); } if (!url) { show.error("url为空!"); throw new Error("url为空!"); } url.includes("http") || (url = window.location.origin + url); const carList = await this.forage.getItem(this.car_list_key) || []; let carData = carList.find((item => item.carNum === carNum)); if (carData) carData.createDate = utils.getNowStr(); else { carData = { carNum: carNum, url: url, actress: actress, status: "", createDate: utils.getNowStr() }; carList.push(carData); } switch (actionType) { case Status_FILTER: if (carData.status === Status_FILTER) { const msg2 = `${carNum} 已在屏蔽列表中`; show.error(msg2); throw new Error(msg2); } carData.status = Status_FILTER; break; case Status_FAVORITE: if (carData.status === Status_FAVORITE) { const msg2 = `${carNum} 已在收藏列表中`; show.error(msg2); throw new Error(msg2); } carData.status = Status_FAVORITE; break; case Status_HAS_DOWN: carData.status = Status_HAS_DOWN; break; default: const msg = "actionType错误"; show.error(msg); throw new Error(msg); } await this.forage.setItem(this.car_list_key, carList); } async getCarList() { return (await this.forage.getItem(this.car_list_key) || []).sort(((a, b) => { if (!a || !b) return 0; const dateA = a.createDate ? new Date(a.createDate).getTime() : 0; return (b.createDate ? new Date(b.createDate).getTime() : 0) - dateA; })); } async getCar(carNum) { return (await this.getCarList()).find((item => item.carNum === carNum)); } async removeCar(carNum) { const carList = await this.getCarList(), initialLength = carList.length, updatedList = carList.filter((car => car.carNum !== carNum)); if (updatedList.length === initialLength) { show.error(`${carNum} 不存在`); return !1; } await this.forage.setItem(this.car_list_key, updatedList); return !0; } async overrideCarList(newList) { if (!Array.isArray(newList)) throw new TypeError("必须传入数组类型数据"); const invalidItems = newList.filter((item => !item || "object" != typeof item || !item.carNum)); if (invalidItems.length > 0) throw new Error(`缺少必要字段 carNum 的数据项: ${invalidItems.length} 条`); const carNums = new Set, duplicates = newList.filter((item => { if (carNums.has(item.carNum)) return !0; carNums.add(item.carNum); return !1; })); if (duplicates.length > 0) throw new Error(`发现重复: ${duplicates.slice(0, 3).map((d => d.carNum)).join(", ")}${duplicates.length > 3 ? "..." : ""}`); await this.forage.setItem(this.car_list_key, newList); } async getItem(key) { if (this.interceptedKeys.includes(key)) { let errorMsg = `危险操作, 改key已有方法实现获取, 请用内部方法调用! key: ${key}`; show.error(errorMsg); throw new Error(errorMsg); } const storedData = await this.forage.getItem(key); if (null == storedData) return null; if ("object" == typeof storedData && "expires" in storedData && "expiresStr" in storedData) { if (Date.now() > storedData.expires) { await this.forage.removeItem(key); return null; } return storedData.value; } return storedData; } async setItem(key, value, maxAge = null) { if (this.interceptedKeys.includes(key)) { let errorMsg = `危险操作, 改key已有方法实现获取, 请用内部方法调用! key: ${key}`; show.error(errorMsg); throw new Error(errorMsg); } let data = value; if (null !== maxAge) { const expires = Date.now() + maxAge; data = { value: value, expires: expires, expiresStr: utils.formatDate(new Date(expires)) }; } return await this.forage.setItem(key, data); } async removeItem(key) { if (this.interceptedKeys.includes(key)) { let errorMsg = `危险操作, 改key不可删除! key: ${key}`; show.error(errorMsg); throw new Error(errorMsg); } return await this.forage.removeItem(key); } async importData(dataJson) { let arrayData = dataJson.filterKeywordList; Array.isArray(arrayData) && await this.forage.setItem(this.title_filter_keyword_key, arrayData); arrayData = dataJson.filterActorList; Array.isArray(arrayData) && await this.forage.setItem(this.filter_actor_key, arrayData); arrayData = dataJson.reviewKeywordList; Array.isArray(arrayData) && await this.forage.setItem(this.review_filter_keyword_key, arrayData); dataJson.dataList && await this.overrideCarList(dataJson.dataList); arrayData = dataJson[this.title_filter_keyword_key]; Array.isArray(arrayData) && await this.forage.setItem(this.title_filter_keyword_key, arrayData); arrayData = dataJson[this.filter_actor_key]; Array.isArray(arrayData) && await this.forage.setItem(this.filter_actor_key, arrayData); arrayData = dataJson[this.review_filter_keyword_key]; Array.isArray(arrayData) && await this.forage.setItem(this.review_filter_keyword_key, arrayData); dataJson[this.car_list_key] && await this.overrideCarList(dataJson[this.car_list_key]); dataJson.setting && await this.saveSetting(dataJson.setting); } async exportData() { return { car_list: await this.getCarList(), filter_actor: await this.getFilterActorList(), title_filter_keyword: await this.getTitleFilterKeyword(), review_filter_keyword: await this.getReviewFilterKeywordList(), setting: await this.getSetting() }; } }; class Utils { constructor() { __publicField(this, "intervalContainer", {}); __publicField(this, "insertStyle", (css => { if (css) { -1 === css.indexOf("<style>") && (css = "<style>" + css + "</style>"); $("head").append(css); } })); Utils.instance || (Utils.instance = this); return Utils.instance; } importResource(url) { let tag; if (url.indexOf("css") >= 0) { tag = document.createElement("link"); tag.setAttribute("rel", "stylesheet"); tag.href = url; } else { tag = document.createElement("script"); tag.setAttribute("type", "text/javascript"); tag.src = url; } document.documentElement.appendChild(tag); } openPage(url, title, shadeClose, event2) { shadeClose || (shadeClose = !0); if (event2 && (event2.ctrlKey || event2.metaKey)) window.open(url); else { url.includes("?") ? url += "&hideNav=1" : url += "?hideNav=1"; layer.open({ type: 2, title: title, content: url, scrollbar: !1, shadeClose: shadeClose, area: [ "80%", "90%" ], isOutAnim: !1, anim: -1 }); } } closePage() { parent.document.documentElement.style.overflow = "auto"; [ ".layui-layer-shade", ".layui-layer-move", ".layui-layer" ].forEach((function(selector) { parent.document.querySelectorAll(selector).forEach((function(el) { el.parentNode.removeChild(el); })); })); window.close(); } loopDetector(condition, after, detectInterval = 20, timeout = 1e4, runWhenTimeout = !0) { let run = !1; const uuid = Math.random(), start = (new Date).getTime(); this.intervalContainer[uuid] = setInterval((() => { if ((new Date).getTime() - start > timeout) { console.warn("loopDetector timeout!", condition, after); run = runWhenTimeout; } if (condition() || run) { clearInterval(this.intervalContainer[uuid]); after && after(); delete this.intervalContainer[uuid]; } }), detectInterval); } rightClick(element, callback) { if (element) { element.jquery ? element = element.toArray() : element instanceof HTMLElement ? element = [ element ] : Array.isArray(element) || (element = [ element ]); element && 0 !== element.length ? element.forEach((el => { el && el.addEventListener("contextmenu", (event2 => { callback(event2); })); })) : console.error("rightClick(), 找不到元素"); } } q(event2, msg, fun, cancelFun) { let x, y; if (event2) { x = event2.clientX - 130; y = event2.clientY - 120; } else { x = window.innerWidth / 2 - 120; y = window.innerHeight / 2 - 120; } let confirmIndex = layer.confirm(msg, { offset: [ y, x ], title: "提示", btn: [ "确定", "取消" ], zIndex: 999999991 }, (function() { fun(); layer.close(confirmIndex); }), (function() { cancelFun && cancelFun(); })); } getNowStr(dateSplitStr = "-", timeSplitStr = ":", dateString = null) { let now; now = dateString ? new Date(dateString) : new Date; const year = now.getFullYear(), month = String(now.getMonth() + 1).padStart(2, "0"), day = String(now.getDate()).padStart(2, "0"), hours = String(now.getHours()).padStart(2, "0"), minutes = String(now.getMinutes()).padStart(2, "0"), seconds = String(now.getSeconds()).padStart(2, "0"); return `${[ year, month, day ].join(dateSplitStr)} ${[ hours, minutes, seconds ].join(timeSplitStr)}`; } formatDate(date, dateSplitStr = "-", timeSplitStr = ":") { let targetDate; if (date instanceof Date) targetDate = date; else { if ("string" != typeof date) throw new Error("Invalid date input: must be Date object or date string"); targetDate = new Date(date); if (isNaN(targetDate.getTime())) throw new Error("Invalid date string"); } const year = targetDate.getFullYear(), month = String(targetDate.getMonth() + 1).padStart(2, "0"), day = String(targetDate.getDate()).padStart(2, "0"), hours = String(targetDate.getHours()).padStart(2, "0"), minutes = String(targetDate.getMinutes()).padStart(2, "0"), seconds = String(targetDate.getSeconds()).padStart(2, "0"); return `${[ year, month, day ].join(dateSplitStr)} ${[ hours, minutes, seconds ].join(timeSplitStr)}`; } download(data, fileName) { const blob = new Blob([ data ], { type: "application/json" }), url = URL.createObjectURL(blob), a = document.createElement("a"); a.href = url; a.download = fileName; document.body.appendChild(a); a.click(); setTimeout((() => { document.body.removeChild(a); URL.revokeObjectURL(url); }), 100); } smoothScrollToTop(duration = 500) { return new Promise((resolve => { const start = performance.now(), startPosition = window.pageYOffset; window.requestAnimationFrame((function scrollStep(timestamp) { const elapsed = timestamp - start, progress = Math.min(elapsed / duration, 1), easeInOutCubic = progress < .5 ? 4 * progress * progress * progress : 1 - Math.pow(-2 * progress + 2, 3) / 2; window.scrollTo(0, startPosition * (1 - easeInOutCubic)); progress < 1 ? window.requestAnimationFrame(scrollStep) : resolve(); })); })); } simpleId() { return Date.now().toString(36) + Math.random().toString(36).substr(2, 5); } log(...data) { console.groupCollapsed("📌", ...data); const stackLines = (new Error).stack.split("\n").slice(2).map((line => line.trim())).filter((line => line.trim())); console.log(stackLines.join("\n")); console.groupEnd(); } isUrl(urlString) { try { new URL(urlString); return !0; } catch (_) { return !1; } } } window.utils = new Utils; window.http = new class { get(url, params = {}, headers = {}) { return this.jqueryRequest("GET", url, null, params, headers); } post(url, data = {}, headers = {}) { return this.jqueryRequest("POST", url, data, null, headers); } put(url, data = {}, headers = {}) { return this.jqueryRequest("PUT", url, data, null, headers); } del(url, params = {}, headers = {}) { return this.jqueryRequest("DELETE", url, null, params, headers); } jqueryRequest(method, url, data = {}, params = {}, headers = {}) { "POST" === method && (headers = { "Content-Type": "application/json", ...headers }); return new Promise(((resolve, reject) => { $.ajax({ method: method, url: url, data: "GET" === method || "DELETE" === method ? params : JSON.stringify(data), headers: headers, success: (response, textStatus, xhr) => { var _a; if (null == (_a = xhr.getResponseHeader("Content-Type")) ? void 0 : _a.includes("application/json")) try { resolve("object" == typeof response ? response : JSON.parse(response)); } catch (e) { resolve(response); } else resolve(response); }, error: (xhr, textStatus, errorThrown) => { let errorMsg = errorThrown; if (xhr.responseText) try { const errorResponse = JSON.parse(xhr.responseText); errorMsg = errorResponse.message || errorResponse.msg || xhr.responseText; } catch { errorMsg = xhr.responseText; } reject(new Error(errorMsg)); } }); })); } }; window.gmHttp = new class { get(url, params = {}, headers = {}) { return this.gmRequest("GET", url, null, params, headers); } post(url, data = {}, headers = {}) { return this.gmRequest("POST", url, data, null, headers); } put(url, data = {}, headers = {}) { return this.gmRequest("PUT", url, data, null, headers); } del(url, params = {}, headers = {}) { return this.gmRequest("DELETE", url, null, params, headers); } gmRequest(method, url, data = {}, params = {}, headers = {}) { if (("GET" === method || "DELETE" === method) && params && Object.keys(params).length) { const queryString = new URLSearchParams(params).toString(); url += (url.includes("?") ? "&" : "?") + queryString; } "POST" !== method && "PUT" !== method || (headers = { "Content-Type": "application/json", ...headers }); return new Promise(((resolve, reject) => { GM_xmlhttpRequest({ method: method, url: url, headers: headers, data: "POST" === method || "PUT" === method ? JSON.stringify(data) : void 0, onload: response => { var _a; try { if (response.status >= 200 && response.status < 300) if (response.responseText && (null == (_a = response.responseHeaders) ? void 0 : _a.toLowerCase().includes("application/json"))) try { resolve(JSON.parse(response.responseText)); } catch (e) { resolve(response.responseText); } else resolve(response.responseText || response); else if (response.responseText) try { const errorData = JSON.parse(response.responseText); reject(errorData); } catch { reject(new Error(response.responseText || `HTTP Error ${response.status}`)); } else reject(new Error(`HTTP Error ${response.status}`)); } catch (e) { reject(e); } }, onerror: error => { reject(new Error(error.error || "Network Error")); }, ontimeout: () => { reject(new Error("Request Timeout")); } }); })); } }; window.storageManager = new StorageManager; const channel = new BroadcastChannel("channel-refresh"); window.refresh = function() { channel.postMessage({ type: "refresh" }); }; !function() { document.head.insertAdjacentHTML("beforeend", '\n <style>\n .loading-container {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: flex;\n justify-content: center;\n align-items: center;\n background-color: rgba(0, 0, 0, 0.1);\n z-index: 99999999;\n }\n \n .loading-animation {\n position: relative;\n width: 60px;\n height: 12px;\n background: linear-gradient(90deg, #4facfe 0%, #00f2fe 100%);\n border-radius: 6px;\n animation: loading-animate 1.8s ease-in-out infinite;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n }\n \n .loading-animation:before,\n .loading-animation:after {\n position: absolute;\n display: block;\n content: "";\n animation: loading-animate 1.8s ease-in-out infinite;\n height: 12px;\n border-radius: 6px;\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);\n }\n \n .loading-animation:before {\n top: -20px;\n left: 10px;\n width: 40px;\n background: linear-gradient(90deg, #ff758c 0%, #ff7eb3 100%);\n }\n \n .loading-animation:after {\n bottom: -20px;\n width: 35px;\n background: linear-gradient(90deg, #ff9a9e 0%, #fad0c4 100%);\n }\n \n @keyframes loading-animate {\n 0% {\n transform: translateX(40px);\n }\n 50% {\n transform: translateX(-30px);\n }\n 100% {\n transform: translateX(40px);\n }\n }\n </style>\n '); window.loading = function() { const container = document.createElement("div"); container.className = "loading-container"; const animation = document.createElement("div"); animation.className = "loading-animation"; container.appendChild(animation); document.body.appendChild(container); return { close: () => { container && container.parentNode && container.parentNode.removeChild(container); } }; }; }(); !function() { document.head.insertAdjacentHTML("beforeend", "\n <style>\n .data-table {\n width: 100%;\n border-collapse: separate;\n border-spacing: 0;\n font-family: 'Helvetica Neue', Arial, sans-serif;\n background: #fff;\n overflow: hidden;\n box-shadow: 0 4px 20px rgba(0, 0, 0, 0.03);\n margin: 0 auto; /* 表格整体水平居中 */\n }\n \n .data-table thead tr {\n background: #f8fafc;\n }\n \n /* 表头居中 */\n .data-table th {\n padding: 16px 20px;\n text-align: center !important; /* 表头文字居中 */\n color: #64748b;\n font-weight: 500;\n font-size: 14px;\n text-transform: uppercase;\n letter-spacing: 0.5px;\n border-bottom: 1px solid #e2e8f0;\n }\n \n /* 单元格内容居中 */\n .data-table td {\n padding: 14px 20px;\n color: #334155;\n font-size: 15px;\n border-bottom: 1px solid #f1f5f9;\n text-align: center !important; /* 单元格文字居中 */\n vertical-align: middle; /* 垂直居中 */\n }\n \n .data-table tbody tr:last-child td {\n border-bottom: none;\n }\n \n /* 行hover 变色*/\n .data-table tbody tr {\n transition: all 0.2s ease;\n }\n \n .data-table tbody tr:hover {\n background: #f8fafc;\n }\n \n /* 可选:特定列左对齐/右对齐的示例 */\n .data-table .text-left {\n text-align: left;\n }\n \n .data-table .text-right {\n text-align: right;\n }\n \n /* 添加.show-border时显示边框 */\n .data-table.show-border {\n border: 1px solid #e2e8f0;\n }\n \n .data-table.show-border th,\n .data-table.show-border td {\n border: 1px solid #e2e8f0;\n }\n </style>\n "); window.TableGenerator = class { constructor(options) { this.defaults = { tableClass: "data-table", showBorder: !1, buttons: [] }; this.config = { ...this.defaults, ...options }; this.validateConfig() && this.init(); } validateConfig() { if (!(this.config.containerId && this.config.columns && Array.isArray(this.config.columns) && Array.isArray(this.config.data))) { console.error("缺少必要参数或参数类型不正确"); return !1; } this.container = document.getElementById(this.config.containerId); if (!this.container) { console.error(`未找到ID为${this.config.containerId}的容器`); return !1; } return !0; } init() { this.container.innerHTML = ""; this.table = document.createElement("table"); this.table.className = this.config.showBorder ? `${this.config.tableClass} show-border` : this.config.tableClass; this.createHeader(); this.createBody(); this.container.appendChild(this.table); } createHeader() { const thead = document.createElement("thead"), headerRow = document.createElement("tr"); this.config.columns.forEach((column => { const th = document.createElement("th"); th.textContent = column.title || column.key; column.width && (th.style.width = column.width); column.headerClass && (th.className = column.headerClass); headerRow.appendChild(th); })); if (this.config.buttons && this.config.buttons.length > 0) { const th = document.createElement("th"); th.textContent = "操作"; this.config.buttonColumnWidth && (th.style.width = this.config.buttonColumnWidth); headerRow.appendChild(th); } thead.appendChild(headerRow); this.table.appendChild(thead); } createBody() { const tbody = document.createElement("tbody"); 0 === this.config.data.length ? this.renderEmptyData(tbody) : this.renderDataRows(tbody); this.table.appendChild(tbody); } renderEmptyData(tbody) { const tr = document.createElement("tr"), td = document.createElement("td"); td.colSpan = this.config.columns.length + (this.config.buttons.length > 0 ? 1 : 0); td.textContent = "暂无数据"; td.style.textAlign = "center"; tr.appendChild(td); tbody.appendChild(tr); } renderDataRows(tbody) { this.config.data.forEach(((item, rowIndex) => { const tr = document.createElement("tr"); this.renderDataCells(tr, item, rowIndex); this.config.buttons && this.config.buttons.length > 0 && this.renderButtonCells(tr, item, rowIndex); tbody.appendChild(tr); })); } renderDataCells(tr, item, rowIndex) { this.config.columns.forEach((column => { const td = document.createElement("td"); column.render ? td.innerHTML = column.render(item, rowIndex) : td.textContent = item[column.key] || ""; column.cellClass && (td.className = column.cellClass); tr.appendChild(td); })); } renderButtonCells(tr, item, rowIndex) { const td = document.createElement("td"); this.config.buttons.forEach((button => { const btn = document.createElement("a"); btn.textContent = button.text; btn.className = button.class || "a-primary"; btn.addEventListener("click", (event2 => { if (button.onClick) { const paramLength = button.onClick.length; 3 === paramLength ? button.onClick(event2, item, rowIndex) : 2 === paramLength ? button.onClick(event2, item) : button.onClick(item); } })); td.appendChild(btn); })); tr.appendChild(td); } update(newData) { this.config.data = newData; this.init(); } getTableElement() { return this.table; } }; }(); !function() { const showMessage = (msg, type, gravityOrOptions, positionOrOptions, options) => { let finalOptions; if ("object" == typeof gravityOrOptions) finalOptions = gravityOrOptions; else { finalOptions = "object" == typeof positionOrOptions ? positionOrOptions : options || {}; finalOptions.gravity = gravityOrOptions || "top"; finalOptions.position = "string" == typeof positionOrOptions ? positionOrOptions : "center"; } finalOptions.gravity && "center" !== finalOptions.gravity || (finalOptions.offset = { y: "calc(50vh - 150px)" }); const colors_infoStart = "#60A5FA", colors_infoEnd = "#93C5FD", colors_successStart = "#10B981", colors_successEnd = "#6EE7B7", colors_errorStart = "#EF4444", colors_errorEnd = "#FCA5A5", commonStyles = { borderRadius: "12px", color: "white", padding: "12px 16px", boxShadow: "0 4px 6px rgba(0,0,0,0.1)", minWidth: "150px", textAlign: "center", zIndex: 999999999 }, defaultConfig = { text: msg, duration: 2e3, close: !1, gravity: "top", position: "center", style: { info: { ...commonStyles, background: `linear-gradient(to right, ${colors_infoStart}, ${colors_infoEnd})` }, success: { ...commonStyles, background: `linear-gradient(to right, ${colors_successStart}, ${colors_successEnd})` }, error: { ...commonStyles, background: `linear-gradient(to right, ${colors_errorStart}, ${colors_errorEnd})` } }[type], stopOnFocus: !0, oldestFirst: !1, ...finalOptions }; Toastify(defaultConfig).showToast(); }; window.show = { ok: (msg, gravityOrOptions = "center", positionOrOptions, options) => { showMessage(msg, "success", gravityOrOptions, positionOrOptions, options); }, error: (msg, gravityOrOptions = "center", positionOrOptions, options) => { showMessage(msg, "error", gravityOrOptions, positionOrOptions, options); }, info: (msg, gravityOrOptions = "center", positionOrOptions, options) => { showMessage(msg, "info", gravityOrOptions, positionOrOptions, options); } }; }(); class PluginManager { constructor() { this.plugins = new Map; } register(pluginClass) { if ("function" != typeof pluginClass) throw new Error("插件必须是一个类"); const name = pluginClass.name; if (!name) throw new Error("类必须要有名称"); const lowerName = name.toLowerCase(); if (this.plugins.has(lowerName)) throw new Error(`插件"${name}"已注册`); const instance = new pluginClass; instance.pluginManager = this; this.plugins.set(lowerName, instance); } getBean(name) { return this.plugins.get(name.toLowerCase()); } _getDependencies(func) { const fnStr = func.toString(); return fnStr.slice(fnStr.indexOf("(") + 1, fnStr.indexOf(")")).split(",").map((arg => arg.trim())).filter((arg => arg)); } async process() { const failedPlugins = (await Promise.allSettled(Array.from(this.plugins).map((async ([name, instance]) => { try { if ("function" == typeof instance.handle) { const css = await instance.initCss(); utils.insertStyle(css); await instance.handle(); return { name: name, status: "fulfilled" }; } console.log("加载插件", name); } catch (e) { console.error(`插件 ${name} 执行失败`, e); return { name: name, status: "rejected", error: e }; } })))).filter((r => "rejected" === r.status)); failedPlugins.length && console.error("以下插件执行失败:", failedPlugins.map((p => p.name))); document.body.classList.add("script-ready"); } } class BasePlugin { constructor() { __publicField(this, "pluginManager", null); } getBean(name) { let bean = this.pluginManager.getBean(name); if (!bean) { let msg = "容器中不存在: " + name; show.error(msg); throw new Error(msg); } return bean; } async initCss() { return ""; } async handle() {} getPageInfo() { let carNum, url, actress, actors, movieId, currentHref = window.location.href; if (isJavDb) { carNum = $('a[title="複製番號"]').attr("data-clipboard-text"); url = currentHref.split("?")[0].split("#")[0]; actress = $(".female").prev().map(((i, el) => $(el).text())).get().join(" "); actors = $(".male").prev().map(((i, el) => $(el).text())).get().join(" "); const parts = window.location.href.split("?")[0].split("/"); movieId = parts[parts.length - 1].split("#")[0]; } if (isJavBus) { url = currentHref.split("?")[0]; carNum = url.split("/").filter(Boolean).pop(); actress = $('span[onmouseover*="star_"] a').map(((i, el) => $(el).text())).get().join(" "); actors = ""; } return { carNum: carNum, url: url, actress: actress, actors: actors, movieId: movieId }; } getSelector() { return isJavDb ? Db : isJavBus ? Bus : null; } } class DetailPagePlugin extends BasePlugin { constructor() { super(); } async initCss() { return window.isDetailPage && window.location.href.includes("hideNav=1") ? "\n .main-nav,#search-bar-container {\n display: none !important;\n }\n \n html {\n padding-top:0px!important;\n }\n " : ""; } handle() { window.isDetailPage && this.checkFilterActor().then(); } async checkFilterActor() { if (!window.isDetailPage) return; const filterActorList = await storageManager.getFilterActorList(); let actors = this.getPageInfo().actors; filterActorList.forEach((item => { if (actors.indexOf(item) > -1) { const detailPageButtonPlugin = this.getBean("detailPageButtonPlugin"); detailPageButtonPlugin.answerCount++; utils.q(null, "存在xxx演员, 是否屏蔽?", (() => { detailPageButtonPlugin.filterOne(null, !0); })); } })); } } class PreviewVideoPlugin extends BasePlugin { async initCss() { return "\n .video-control-btn {\n position: absolute;\n bottom: 10px;\n right: 10px;\n z-index: 99999999999;\n min-width:100px;\n padding: 8px 16px;\n background: rgba(0,0,0,0.7);\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n }\n .video-control-btn.active {\n background-color: #1890ff; /* 选中按钮的背景色 */\n color: white; /* 选中按钮的文字颜色 */\n font-weight: bold; /* 加粗显示 */\n border: 2px solid #096dd9; /* 边框样式 */\n }\n "; } handle() { let $preview = $(".preview-video-container"); $preview.on("click", (event2 => { utils.loopDetector((() => $(".fancybox-content #preview-video").length > 0), (() => { this.handleVideo().then(); })); })); utils.loopDetector((() => $(".fancybox-content #preview-video").length > 0), (() => { $(".fancybox-content #preview-video").length > 0 && this.handleVideo().then(); })); window.location.href.includes("autoPlay=1") && $preview[0].click(); } async handleVideo() { const $videoEl = $("#preview-video"), $previewSource = $videoEl.find("source"), $videoContainer = $videoEl.parent(); if (!$videoEl.length || !$previewSource.length) return; const videoEl = $videoEl[0]; videoEl.muted = !1; $videoContainer.css("position", "relative"); const videoSrc = $previewSource.attr("src"), qualityLevels = [ "hhb", "hmb", "mhb", "mmb" ], currentQuality = qualityLevels.find((q => videoSrc.includes(q))) || "mhb", qualityOptions = [ { id: "video-mmb", text: "低画质", quality: "mmb" }, { id: "video-mhb", text: "中画质", quality: "mhb" }, { id: "video-hmb", text: "高画质", quality: "hmb" }, { id: "video-hhb", text: "超高清", quality: "hhb" } ]; const cacheKey = `videoQualities_${this.getPageInfo().carNum}`; let availableQualities = JSON.parse(sessionStorage.getItem(cacheKey)); if (!availableQualities) { availableQualities = (await Promise.all(qualityOptions.map((async option => { const testSrc = videoSrc.replace(new RegExp(qualityLevels.join("|"), "g"), option.quality); try { return (await fetch(testSrc, { method: "HEAD" })).ok ? option : null; } catch { return null; } })))).filter(Boolean); sessionStorage.setItem(cacheKey, JSON.stringify(availableQualities)); } if (availableQualities.length <= 1) return; const buttonsHtml = availableQualities.map(((option, index) => `\n <button class="video-control-btn${option.quality === currentQuality ? " active" : ""}" \n id="${option.id}" \n data-quality="${option.quality}"\n style="bottom: ${50 * index}px; right: -105px;">\n ${option.text}\n </button>\n `)).join(""); $videoContainer.append(buttonsHtml); const $buttons = $videoContainer.find(".video-control-btn"); $videoContainer.on("click", ".video-control-btn", (async e => { const $button = $(e.currentTarget), quality = $button.data("quality"); if (!$button.hasClass("active")) try { const newSrc = videoSrc.replace(new RegExp(qualityLevels.join("|"), "g"), quality); $previewSource.attr("src", newSrc); videoEl.load(); videoEl.muted = !1; await videoEl.play(); $buttons.removeClass("active"); $button.addClass("active"); } catch (error) { console.error("切换画质失败:", error); } })); $buttons.last().trigger("click"); } } const _HotkeyManager = class _HotkeyManager { constructor() { if (new.target === _HotkeyManager) throw new Error("HotkeyManager cannot be instantiated."); } static registerHotkey(hotkeyString, callback, keyupCallback = null) { if (Array.isArray(hotkeyString)) { let id_list = []; hotkeyString.forEach((hotkey => { if (!this.isHotkeyFormat(hotkey)) throw new Error("快捷键格式错误"); let id = this.recordHotkey(hotkey, callback, keyupCallback); id_list.push(id); })); return id_list; } if (!this.isHotkeyFormat(hotkeyString)) throw new Error("快捷键格式错误"); return this.recordHotkey(hotkeyString, callback, keyupCallback); } static recordHotkey(hotkeyString, callback, keyupCallback) { let id = Math.random().toString(36).substr(2); this.registerHotKeyMap.set(id, { hotkeyString: hotkeyString, callback: callback, keyupCallback: keyupCallback }); return id; } static unregisterHotkey(id) { this.registerHotKeyMap.has(id) && this.registerHotKeyMap.delete(id); } static isHotkeyFormat(hotkeyString) { return hotkeyString.toLowerCase().split("+").map((k => k.trim())).every((k => [ "ctrl", "shift", "alt" ].includes(k) || 1 === k.length)); } static judgeHotkey(hotkeyString, event2) { const keyList = hotkeyString.toLowerCase().split("+").map((k => k.trim())), ctrl = keyList.includes("ctrl"), shift = keyList.includes("shift"), alt = keyList.includes("alt"), key = keyList.find((k => "ctrl" !== k && "shift" !== k && "alt" !== k)); return (this.isMac ? event2.metaKey : event2.ctrlKey) === ctrl && event2.shiftKey === shift && event2.altKey === alt && event2.key.toLowerCase() === key; } }; __publicField(_HotkeyManager, "isMac", 0 === navigator.platform.indexOf("Mac")); __publicField(_HotkeyManager, "registerHotKeyMap", new Map); __publicField(_HotkeyManager, "handleKeydown", (event2 => { for (const [id, data] of _HotkeyManager.registerHotKeyMap) { let hotkeyString = data.hotkeyString, callback = data.callback; _HotkeyManager.judgeHotkey(hotkeyString, event2) && callback(event2); } })); __publicField(_HotkeyManager, "handleKeyup", (event2 => { for (const [id, data] of _HotkeyManager.registerHotKeyMap) { let hotkeyString = data.hotkeyString, keyupCallback = data.keyupCallback; keyupCallback && (_HotkeyManager.judgeHotkey(hotkeyString, event2) && keyupCallback(event2)); } })); let HotkeyManager = _HotkeyManager; document.addEventListener("keydown", (event2 => { HotkeyManager.handleKeydown(event2); })); document.addEventListener("keyup", (event2 => { HotkeyManager.handleKeyup(event2); })); class JavTrailersPlugin extends BasePlugin { constructor() { super(); this.hasBand = !1; } handle() { let href = window.location.href; if (!href.includes("handle=1")) return; if ($("h1:contains('Page not found')").length) { let keyword = href.split("?")[0].split("video/")[1].toLowerCase().replace("00", "-"); window.location.href = "https://javtrailers.com/search/" + keyword + "?handle=1"; return; } let findList = $(".videos-list .video-link").toArray(); if (findList.length) { const keyword = href.split("?")[0].split("search/")[1].toLowerCase(), matchedLink = findList.find((el => $(el).find(".vid-title").text().toLowerCase().includes(keyword))); if (matchedLink) { window.location.href = $(matchedLink).attr("href") + "?handle=1"; return; } } this.handlePlayJavTrailers(); $("#videoPlayerContainer").on("click", (() => { this.handlePlayJavTrailers(); })); window.addEventListener("message", (event2 => { let videoEl = document.getElementById("vjs_video_3_html5_api"); videoEl && (videoEl.currentTime += 5); })); HotkeyManager.registerHotkey("z", (() => { const videoEl = document.getElementById("vjs_video_3_html5_api"); videoEl && (videoEl.currentTime += 5); })); HotkeyManager.registerHotkey("a", (() => window.parent.postMessage("a", "*"))); HotkeyManager.registerHotkey("s", (() => window.parent.postMessage("s", "*"))); } handlePlayJavTrailers() { this.hasBand || utils.loopDetector((() => 0 !== $("#vjs_video_3_html5_api").length), (() => { setTimeout((() => { this.hasBand = !0; let videoEl = document.getElementById("vjs_video_3_html5_api"); videoEl.play(); videoEl.currentTime = 5; videoEl.addEventListener("timeupdate", (function() { videoEl.currentTime >= 14 && videoEl.currentTime < 16 && (videoEl.currentTime += 2); })); $("#vjs_video_3_html5_api").css({ position: "fixed", width: "100vw", height: "100vh", objectFit: "cover", zIndex: "999999999" }); $(".vjs-control-bar").css({ position: "fixed", bottom: "20px", zIndex: "999999999" }); }), 0); })); } } class SubTitleCatPlugin extends BasePlugin { handle() { $(".t-banner-inner").hide(); $("#navbar").hide(); let keyword = window.location.href.split("=")[1].toLowerCase(); $(".sub-table tr td a").toArray().forEach((el => { let item = $(el); item.text().toLowerCase().includes(keyword) || item.parent().parent().hide(); })); } } const apiUrl = "https://jdforrepam.com/api"; async function buildSignature() { const curr = Math.floor(Date.now() / 1e3); if (curr - (await storageManager.getItem(storageManager.review_ts_key) || 0) <= 20) return await storageManager.getItem(storageManager.review_sign_key); const sign = `${curr}.lpw6vgqzsp.${md5(`${curr}71cf27bb3c0bcdf207b64abecddc970098c7421ee7203b9cdae54478478a199e7d5a6e1a57691123c1a931c057842fb73ba3b3c83bcd69c17ccf174081e3d8aa`)}`; await storageManager.setItem(storageManager.review_ts_key, curr); await storageManager.setItem(storageManager.review_sign_key, sign); return sign; } const getReviews = async (movieId, pageNum = 1, pageSize = 20) => { let url = `${apiUrl}/v1/movies/${movieId}/reviews`, headers = { jdSignature: await buildSignature() }; return (await http.get(url, { page: pageNum, sort_by: "hotly", limit: pageSize }, headers)).data.reviews; }, getMovieDetail = async movieId => { let url = `${apiUrl}/v4/movies/${movieId}`, headers = { jdSignature: await buildSignature() }; const res = await http.get(url, null, headers); if (!res.data) { show.error("获取视频详情失败: " + res.message); throw new Error(res.message); } const movie = res.data.movie, preview_images = movie.preview_images, imgList = []; preview_images.forEach((item => { imgList.push(item.large_url.replace("https://tp-iu.cmastd.com/rhe951l4q", "https://c0.jdbstatic.com")); })); return { movieId: movie.id, actors: movie.actors, title: movie.origin_title, carNum: movie.number, score: movie.score, releaseDate: movie.release_date, watchedCount: movie.watched_count, imgList: imgList }; }, related = async (movieId, page = 1, limit = 20) => { let url = `${apiUrl}/v1/lists/related?movie_id=${movieId}&page=${page}&limit=${limit}`, headers = { jdSignature: await buildSignature() }; const res = await gmHttp.get(url, null, headers), dataList = []; res.data.lists.forEach((item => { dataList.push({ relatedId: item.id, name: item.name, movieCount: item.movies_count, collectionCount: item.collections_count, viewCount: item.views_count, createTime: utils.formatDate(item.created_at) }); })); return dataList; }; class Fc2Plugin extends BasePlugin { handle() { let fc2Url = "/advanced_search?type=3&score_min=3&d=1"; $('.navbar-item:contains("FC2")').attr("href", fc2Url); $('.tabs a:contains("FC2")').attr("href", fc2Url); if (window.location.href.includes("collection_codes?movieId")) { const urlParams = new URLSearchParams(window.location.search); let movieId = urlParams.get("movieId"), carNum = urlParams.get("carNum"), url = urlParams.get("url"); movieId && carNum && url && this.openFc2Page(movieId, carNum, url); } } async initCss() { return "\n /* 弹层样式 */\n .movie-detail-layer .layui-layer-title {\n font-size: 18px;\n color: #333;\n background: #f8f8f8;\n }\n \n \n /* 容器样式 */\n .movie-detail-container {\n display: flex;\n height: 100%;\n background: #fff;\n }\n \n .movie-poster-container {\n flex: 0 0 60%;\n padding: 15px;\n }\n \n .right-box {\n flex: 1;\n padding: 20px;\n overflow-y: auto;\n }\n \n /* 预告片iframe */\n .movie-trailer {\n width: 100%;\n height: 100%;\n min-height: 400px;\n background: #000;\n border-radius: 4px;\n }\n \n /* 电影信息样式 */\n .movie-title {\n font-size: 24px;\n margin-bottom: 15px;\n color: #333;\n }\n \n .movie-meta {\n margin-bottom: 20px;\n color: #666;\n }\n \n .movie-meta span {\n margin-right: 15px;\n }\n \n /* 演员列表 */\n .actor-list {\n display: flex;\n flex-wrap: wrap;\n gap: 8px;\n margin-top: 10px;\n }\n \n .actor-tag {\n padding: 4px 12px;\n background: #f0f0f0;\n border-radius: 15px;\n font-size: 12px;\n color: #555;\n }\n \n /* 图片列表 */\n .image-list {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-top: 10px;\n }\n \n .movie-image-thumb {\n width: 120px;\n height: 80px;\n object-fit: cover;\n border-radius: 4px;\n cursor: pointer;\n transition: transform 0.3s;\n }\n \n .movie-image-thumb:hover {\n transform: scale(1.05);\n }\n \n /* 加载中和错误状态 */\n .search-loading, .movie-error {\n padding: 40px;\n text-align: center;\n color: #999;\n }\n \n .movie-error {\n color: #f56c6c;\n }\n \n .fancybox-container{\n z-index:99999999\n }\n \n \n /* 错误提示样式 */\n .movie-not-found, .movie-error {\n text-align: center;\n padding: 30px;\n color: #666;\n }\n \n .movie-not-found h3, .movie-error h3 {\n color: #f56c6c;\n margin: 15px 0;\n }\n \n .icon-warning, .icon-error {\n font-size: 50px;\n color: #e6a23c;\n }\n \n .icon-error {\n color: #f56c6c;\n }\n\n "; } openFc2Page(movieId, carNum, href) { layer.open({ type: 1, title: "影片详情", content: '\n <div class="movie-detail-container">\n <div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>\n <div class="right-box">\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n <div style="margin: 10px 0">\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>收藏</span></a>\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333"><span>屏蔽</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>加入已下载</span></a>\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links" style="margin: 0 0.75rem">\n <div class="search-loading">加载中...</div>\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n </div>\n </div>\n ', area: [ "80%", "90%" ], skin: "movie-detail-layer", scrollbar: !1, success: (layero, index) => { this.loadData(movieId, carNum); $("#favoriteBtn").on("click", (async event2 => { const actress = $("#data-actress").text(); await storageManager.saveCar(carNum, href, actress, Status_FAVORITE); window.refresh(); layer.closeAll(); })); $("#filterBtn").on("click", (event2 => { utils.q(event2, `是否屏蔽${carNum}?`, (async () => { const actress = $("#data-actress").text(); await storageManager.saveCar(carNum, href, actress, Status_FILTER); window.refresh(); layer.closeAll(); window.location.href.includes("collection_codes?movieId") && utils.closePage(); })); })); $("#hasDownBtn").on("click", (async event2 => { const actress = $("#data-actress").text(); await storageManager.saveCar(carNum, href, actress, Status_HAS_DOWN); window.refresh(); layer.closeAll(); })); }, end() { window.location.href.includes("collection_codes?movieId") && utils.closePage(); } }); } loadData(movieId, carNum) { this.handleVideo(carNum.replace("FC2-", "")); this.handleMovieDetail(movieId); this.handleMagnets(movieId); this.getBean("reviewPlugin").showReview(movieId, $("#reviews-content")).then(); } handleMovieDetail(movieId) { getMovieDetail(movieId).then((res => { const actors = res.actors || [], imgList = res.imgList || []; let actorsHtml = ""; if (actors.length > 0) { let actress = ""; for (let i = 0; i < actors.length; i++) { let actor = actors[i]; actorsHtml += `<span class="actor-tag"><a href="/actors/${actor.id}" target="_blank">${actor.name}</a></span>`; 0 === actor.gender && (actress += actor.name); } $("#data-actress").text(actress); } else actorsHtml = '<span class="no-data">暂无演员信息</span>'; let imagesHtml = ""; imagesHtml = Array.isArray(imgList) && imgList.length > 0 ? imgList.map(((img, index) => `\n <a href="${img}" data-fancybox="movie-gallery" data-caption="剧照 ${index + 1}">\n <img src="${img}" class="movie-image-thumb" alt=""/>\n </a>\n `)).join("") : '<div class="no-data">暂无剧照</div>'; $(".movie-info-container").html(`\n <h3 class="movie-title">${res.title || "无标题"}</h3>\n <div class="movie-meta">\n <span>番号: ${res.carNum || "未知"}</span>\n <span>年份: ${res.releaseDate || "未知"}</span>\n <span>评分: ${res.score || "无"}</span>\n </div>\n <div class="movie-actors">\n <div class="actor-list">主演: ${actorsHtml}</div>\n </div>\n <div class="movie-gallery" style="margin-top:10px">\n <h4>剧照: </h4>\n <div class="image-list">${imagesHtml}</div>\n </div>\n `); })).catch((err => { console.error(err); $(".movie-info-container").html(`\n <div class="movie-error">加载失败: ${err.message}</div>\n `); })); } handleMagnets(movieId) { (async movieId => { let url = `${apiUrl}/v1/movies/${movieId}/magnets`, headers = { jdSignature: await buildSignature() }; return (await http.get(url, null, headers)).data.magnets; })(movieId).then((magnetList => { let magnetsHtml = ""; if (magnetList.length > 0) for (let i = 0; i < magnetList.length; i++) { let magnet = magnetList[i], oddClass = ""; i % 2 == 0 && (oddClass = "odd"); magnetsHtml += `\n <div class="item columns is-desktop ${oddClass}">\n <div class="magnet-name column is-four-fifths">\n <a href="magnet:?xt=urn:btih:${magnet.hash}" title="右鍵點擊並選擇「複製鏈接地址」">\n <span class="name">${magnet.name}</span>\n <br>\n <span class="meta">\n ${(magnet.size / 1024).toFixed(2)}GB, ${magnet.files_count}個文件 \n </span>\n <br>\n <div class="tags">\n ${magnet.hd ? '<span class="tag is-primary is-small is-light">高清</span>' : ""}\n ${magnet.cnsub ? '<span class="tag is-warning is-small is-light">字幕</span>' : ""}\n </div>\n </a>\n </div>\n <div class="buttons column">\n <button class="button is-info is-small copy-to-clipboard" data-clipboard-text="magnet:?xt=urn:btih:${magnet.hash}" type="button"> 複製 </button>\n </div>\n <div class="date column"><span class="time">${magnet.created_at}</span></div>\n </div>\n `; } else magnetsHtml = '<span class="no-data">暂无磁力信息</span>'; $("#magnets-content").html(magnetsHtml); })).catch((err => { console.error(err); $("#magnets-content").html(`\n <div class="movie-error">加载失败: ${err.message}</div>\n `); })); } handleVideo(searchKeyword) { (async carNum => { let url = `https://hohoj.tv/search?text=${carNum}`, html = await gmHttp.get(url), pageUrl = null; if (html.includes("找不到任何影片")) return pageUrl; const doc = (new DOMParser).parseFromString(html, "text/html"); $(doc).find(".video-item a").toArray().forEach((item => { if ($(item).find(".video-item-title").text().includes(carNum)) { let pageId = $(item).attr("href").split("id=")[1]; pageUrl = "https://hohoj.tv/embed?id=" + pageId; } })); return pageUrl; })(searchKeyword).then((pageUrl => { const moviePosterContainer = document.querySelector(".movie-poster-container"), iframe = document.querySelector(".movie-trailer"); if (pageUrl) $(iframe).attr("src", pageUrl); else { moviePosterContainer.innerHTML = `\n <div class="movie-not-found">\n <i class="icon-warning"></i>\n <h3>未找到相关内容</h3>\n <p>hohoj.tv 中没有找到与当前番号相关的影片信息</p>\n <p style="margin:20px">请尝试以下网站</p>\n <p><a class="menu-btn" style="background:linear-gradient(to right, #d29494, rgb(254,98,142))" href="https://missav.ws/dm3/fc2-ppv-${searchKeyword}" target="_blank">missav</a></p>\n </div>\n `; iframe.style.display = "none"; } })); } } class FoldCategoryPlugin extends BasePlugin { async handle() { if (!window.isListPage) return; let $subTags, $topTabs = $(".tabs ul"); if ($topTabs.length > 0) { $subTags = $("#tags"); let checkTagStr = $("#tags dl div.tag.is-info").map((function() { return $(this).text().replaceAll("\n", "").replaceAll(" ", ""); })).get().join(" "); if (!checkTagStr) return; $topTabs.append('\n <li class="is-active" id="foldCategoryBtn">\n <a class="menu-btn" style="background-color:#d23e60 !important;margin-left: 20px;border-bottom:none !important;border-radius:3px;">\n <span></span>\n <i style="margin-left: 10px"></i>\n </a>\n </li>\n '); $(".tabs").append(`<div style="padding-top:10px"><span>已选分类: ${checkTagStr}</span></div>`); } let $section = $("h2.section-title"); if ($section.length > 0) { $section.append('\n <div id="foldCategoryBtn">\n <a class="menu-btn" style="background-color:#d23e60 !important;margin-left: 20px;border-bottom:none !important;border-radius:3px;">\n <span></span>\n <i style="margin-left: 10px"></i>\n </a>\n </div>\n '); $subTags = $("section > div > div.box"); } if (!$subTags) return; let $foldCategoryBtn = $("#foldCategoryBtn"), isFolded = "yes" === await storageManager.getItem(storageManager.fold_category_key), [newText, newIcon] = isFolded ? [ "展开", "icon-angle-double-down" ] : [ "折叠", "icon-angle-double-up" ]; $foldCategoryBtn.find("span").text(newText).end().find("i").attr("class", newIcon); window.location.href.includes("noFold=1") || $subTags[isFolded ? "hide" : "show"](); $foldCategoryBtn.on("click", (async event2 => { event2.preventDefault(); isFolded = !isFolded; await storageManager.setItem(storageManager.fold_category_key, isFolded ? "yes" : "no"); const [newText2, newIcon2] = isFolded ? [ "展开", "icon-angle-double-down" ] : [ "折叠", "icon-angle-double-up" ]; $foldCategoryBtn.find("span").text(newText2).end().find("i").attr("class", newIcon2); $subTags[isFolded ? "hide" : "show"](); })); } } class ActressInfoPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "apiUrl", "https://ja.wikipedia.org/wiki/"); } handle() { this.handleDetailPage().then(); this.handleStarPage().then(); } async initCss() { return "\n <style>\n .info-tag {\n background-color: #ecf5ff;\n display: inline-block;\n height: 32px;\n padding: 0 10px;\n line-height: 30px;\n font-size: 12px;\n color: #409eff;\n border: 1px solid #d9ecff;\n border-radius: 4px;\n box-sizing: border-box;\n white-space: nowrap;\n }\n </style>\n "; } async handleDetailPage() { let nameList = $(".female").prev().map(((i, el) => $(el).text().trim())).get(); if (!nameList.length) return; let result = null, infoHtml = ""; for (let i = 0; i < nameList.length; i++) { let name = nameList[i]; result = await storageManager.getItem(storageManager.actress_prefix_key + name); if (!result) try { result = await this.searchInfo(name); result && await storageManager.setItem(storageManager.actress_prefix_key + name, result, 2592e6); } catch (e) { console.error("该名称查询失败,尝试其它名称"); } let contentHtml = ""; contentHtml = result ? `\n <div class="panel-block">\n <strong>${name}:</strong>\n <a href="${result.url}" style="margin-left: 5px" target="_blank">\n <span class="info-tag">${result.birthday} ${result.age}</span>\n <span class="info-tag">${result.height} ${result.weight}</span>\n <span class="info-tag">${result.threeSizeText} ${result.braSize}</span>\n </a>\n </div>\n ` : `<div class="panel-block"><a href="${this.apiUrl + name}" target="_blank"><strong>${name}:</strong></a></div> `; infoHtml += contentHtml; } $('strong:contains("演員")').parent().after(infoHtml); } async handleStarPage() { let nameList = [], $actor = $(".actor-section-name"); $actor.length && $actor.text().trim().split(",").forEach((name => { nameList.push(name.trim()); })); let $sectionMeta = $(".section-meta:not(:contains('影片'))"); $sectionMeta.length && $sectionMeta.text().trim().split(",").forEach((name => { nameList.push(name.trim()); })); if (!nameList.length) return; let result = null; for (let i = 0; i < nameList.length; i++) { let name = nameList[i]; result = await storageManager.getItem(storageManager.actress_prefix_key + name); if (result) break; try { result = await this.searchInfo(name); } catch (e) { console.error("该名称查询失败,尝试其它名称"); } if (result) break; } result && nameList.forEach((name => { storageManager.setItem(storageManager.actress_prefix_key + name, result, 2592e6); })); let contentHtml = '<div style="font-size: 17px; font-weight: normal; margin-top: 5px;">无此相关演员信息</div>'; result && (contentHtml = `\n <a href="${result.url}" target="_blank">\n <div style="font-size: 17px; font-weight: normal; margin-top: 5px;">\n <div style="display: flex; margin-bottom: 10px;">\n <span style="width: 300px;">出生日期: ${result.birthday}</span>\n <span style="width: 200px;">年龄: ${result.age}</span>\n <span style="width: 200px;">身高: ${result.height}</span>\n </div>\n <div style="display: flex; margin-bottom: 10px;">\n <span style="width: 300px;">体重: ${result.weight}</span>\n <span style="width: 200px;">三围: ${result.threeSizeText}</span>\n <span style="width: 200px;">罩杯: ${result.braSize}</span>\n </div>\n </div>\n </a>\n `); $actor.parent().append(contentHtml); } async searchInfo(name) { "三上悠亞" === name && (name = "三上悠亜"); let url = this.apiUrl + name; const html = await gmHttp.get(url), parser = new DOMParser, $dom = $(parser.parseFromString(html, "text/html")); let birthday = $dom.find('tr:has(a[title="誕生日"]) td').text().trim(), age = $dom.find("th:contains('現年齢')").parent().find("td").text().trim() ? parseInt($dom.find("th:contains('現年齢')").parent().find("td").text().trim()) + "岁" : "", height = $dom.find('tr:has(a[title="身長"]) td').text().trim().split(" ")[0] + "cm", weight = $dom.find('tr:has(a[title="体重"]) td').text().trim().split("/")[1].trim(); "― kg" === weight && (weight = ""); return { birthday: birthday, age: age, height: height, weight: weight, threeSizeText: $dom.find('a[title="スリーサイズ"]').closest("tr").find("td").text().replace("cm", "").trim(), braSize: $dom.find('th:contains("ブラサイズ")').next("td").contents().first().text().trim(), url: url }; } } class AliyunPanPlugin extends BasePlugin { handle() { $("body").append('<a class="a-success" id="refresh-token-btn" style="position:fixed; right: 0; top:50%;z-index:99999">获取refresh_token</a>'); $("#refresh-token-btn").on("click", (event2 => { let tokenStr = localStorage.getItem("token"); if (!tokenStr) { alert("请先登录!"); return; } let refresh_token = JSON.parse(tokenStr).refresh_token; navigator.clipboard.writeText(refresh_token).then((() => { alert("已复制到剪切板 如失败, 请手动复制: " + refresh_token); })).catch((err => { console.error("Failed to copy refresh token: ", err); })); })); } } class HitShowPlugin extends BasePlugin { constructor() { super(); } handle() { $('a[href*="rankings/playback"]').on("click", (event2 => { event2.preventDefault(); event2.stopPropagation(); window.location.href = "/?handlePlayback=1&period=daily"; })); this.handlePlayback().then(); } async handlePlayback() { if (!window.location.href.includes("handlePlayback=1")) return; let period = new URLSearchParams(window.location.search).get("period"); this.toolBar(period); let $movieBox = $(".movie-list"); $movieBox.html(""); let loadObj = loading(); try { const movies = await (async (period = "daily", filter_by = "high_score") => { let url = `${apiUrl}/v1/rankings/playback?period=${period}&filter_by=${filter_by}`, headers = { jdSignature: await buildSignature() }; return (await http.get(url, null, headers)).data.movies; })(period); let moviesHtml = this.markDataListHtml(movies); $movieBox.html(moviesHtml); window.refresh(); this.loadScore(movies); } finally { loadObj.close(); } } toolBar(period) { $(".pagination").remove(); $(".main-tabs ul li").removeClass("is-active"); $(".main-tabs ul li:first").addClass("is-active"); let conditionHtml = `\n <div class="button-group" style="margin-top:18px">\n <div class="buttons has-addons" id="conditionBox">\n <a style="padding:18px 18px !important;" class="button is-small ${"daily" === period ? "is-info" : ""}" href="/?handlePlayback=1&period=daily">日榜</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"weekly" === period ? "is-info" : ""}" href="/?handlePlayback=1&period=weekly">周榜</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"monthly" === period ? "is-info" : ""}" href="/?handlePlayback=1&period=monthly">月榜</a>\n </div>\n </div>\n `; $(".toolbar").html(conditionHtml); } getStarRating(score) { let stars = ""; const fullStars = Math.floor(score); for (let i = 0; i < fullStars; i++) stars += '<i class="icon-star"></i>'; for (let i = 0; i < 5 - fullStars; i++) stars += '<i class="icon-star gray"></i>'; return stars; } loadScore(movies) { if (0 === movies.length) return; (async () => { const errors = []; for (const movie of movies) try { const movieId = movie.id; if ($(`#${movieId}`).is(":hidden")) continue; const cached = await storageManager.getItem(storageManager.score_prefix_key + movieId); if (cached) { this.appendScoreHtml(movieId, cached); continue; } for (;!document.hasFocus(); ) await new Promise((r => setTimeout(r, 500))); const res = await getMovieDetail(movieId); let score = res.score, watchedCount = res.watchedCount, html = `\n <span class="value">\n <span class="score-stars">${this.getStarRating(score)}</span> \n ${score}分,由${watchedCount}人評價\n </span>\n `; this.appendScoreHtml(movieId, html); await storageManager.setItem(storageManager.score_prefix_key + movieId, html, 6048e5); await new Promise((r => setTimeout(r, 1e3))); } catch (err) { errors.push({ carNum: movie.number, error: err.message, stack: err.stack }); console.error(`🚨 解析评分数据失败 | 编号: ${movie.number}\n`, `错误详情: ${err.message}\n`, err.stack ? `调用栈:\n${err.stack}` : ""); } if (errors.length > 0) { show.error("解析评分数据失败, 个数:", errors.length); console.table(errors); } })(); } appendScoreHtml(movieId, scoreHtml) { let $scoreBox = $(`#score_${movieId}`); "" === $scoreBox.html().trim() && $scoreBox.slideUp(0, (function() { $(this).html(scoreHtml).slideDown(500); })); } markDataListHtml(movies) { let moviesHtml = ""; movies.forEach((movie => { moviesHtml += `\n <div class="item" id="${movie.id}">\n <a href="/v/${movie.id}" class="box" title="${movie.origin_title}">\n <div class="cover ">\n <img loading="lazy" src="${movie.cover_url.replace("https://tp-iu.cmastd.com/rhe951l4q", "https://c0.jdbstatic.com")}" alt="">\n </div>\n <div class="video-title"><strong>${movie.number}</strong> ${movie.origin_title}</div>\n <div class="score" id="score_${movie.id}">\n </div>\n <div class="meta">\n ${movie.release_date}\n </div>\n <div class="tags has-addons">\n ${movie.has_cnsub ? '<span class="tag is-warning">含中字磁鏈</span>' : movie.magnets_count > 0 ? '<span class="tag is-success">含磁鏈</span>' : '<span class="tag is-info">无磁鏈</span>'}\n ${movie.new_magnets ? '<span class="tag is-info">今日新種</span>' : ""}\n </div>\n </a>\n </div>\n `; })); return moviesHtml; } } class TOP250Plugin extends BasePlugin { constructor() { super(); __publicField(this, "has_cnsub", ""); __publicField(this, "movies", []); } handle() { $('.main-tabs ul li:contains("猜你喜歡")').html('<a href="/rankings/top"><span>Top250</span></a>'); $('a[href*="rankings/top"]').on("click", (event2 => { event2.preventDefault(); event2.stopPropagation(); const $target = $(event2.target), href = ($target.is("a") ? $target : $target.closest("a")).attr("href"); let queryString = href.includes("?") ? href.split("?")[1] : href; const urlParams = new URLSearchParams(queryString); this.checkLogin(event2, urlParams); })); this.handleTop().then(); } async handleTop() { if (!window.location.href.includes("handleTop=1")) return; const urlParams = new URLSearchParams(window.location.search); let type = urlParams.get("type") || "all", type_value = urlParams.get("type_value") || ""; this.has_cnsub = urlParams.get("has_cnsub") || ""; let page = urlParams.get("page") || 1; this.toolBar(type, type_value, page); let $movieBox = $(".movie-list"); $movieBox.html(""); let loadObj = loading(); try { const res = await (async (type = "all", type_value = "", page = 1, limit = 40) => { let url = `${apiUrl}/v1/movies/top?start_rank=1&type=${type}&type_value=${type_value}&ignore_watched=false&page=${page}&limit=${limit}`, headers = { "user-agent": "Dart/3.5 (dart:io)", "accept-language": "zh-TW", host: "jdforrepam.com", authorization: "Bearer " + await storageManager.getItem("appAuthorization"), jdsignature: await buildSignature() }; return await gmHttp.get(url, null, headers); })(type, type_value, page, 50); let success = res.success, message = res.message, action = res.action; if (1 === success) { let movies = res.data.movies; if (0 === movies.length) { show.error("无数据"); return; } this.movies = movies; const hitShowPlugin = this.getBean("hitShowPlugin"); let moviesHtml = hitShowPlugin.markDataListHtml(movies); $movieBox.html(moviesHtml); window.refresh(); if ("1" === this.has_cnsub) { $(".item:contains('含中字磁鏈')").show(); $(".item:contains('含磁鏈')").hide(); } else if ("0" === this.has_cnsub) { $(".item:contains('含中字磁鏈')").hide(); $(".item:contains('含磁鏈')").show(); } else { $(".item:contains('含中字磁鏈')").show(); $(".item:contains('含磁鏈')").show(); } hitShowPlugin.loadScore(movies); } else { console.error(res); $movieBox.html(`<h3>${message}</h3>`); show.error(message); } if ("JWTVerificationError" === action) { await storageManager.removeItem("appAuthorization"); await this.checkLogin(null, new URLSearchParams(window.location.search)); } } catch (e) { console.error("获取Top数据失败:", e); show.error(`获取Top数据失败: ${e ? e.message : e}`); } finally { loadObj.close(); } } toolBar(type, type_value, currentPage) { $(".main-tabs ul li").removeClass("is-active"); $(".main-tabs ul li:eq(1)").addClass("is-active"); if ("5" === currentPage.toString()) { $("#auto-page").remove(); $(".pagination-next").remove(); } $(".pagination-ellipsis").closest("li").remove(); $(".pagination-list li a").each((function() { parseInt($(this).text()) > 5 && $(this).closest("li").remove(); })); let yearHtml = ""; for (let year = (new Date).getFullYear(); year >= 2008; year--) yearHtml += `\n <a style="padding:18px 18px !important;" \n class="button is-small ${type_value === year.toString() ? "is-info" : ""}" \n href="/?handleTop=1&type=year&type_value=${year}&has_cnsub=${this.has_cnsub}">\n ${year}\n </a>\n `; let conditionHtml = `\n <div class="button-group">\n <div class="buttons has-addons" id="conditionBox" style="margin-bottom: 0!important;">\n <a style="padding:18px 18px !important;" class="button is-small ${"all" === type ? "is-info" : ""}" href="/?handleTop=1&type=all&type_value=&has_cnsub=${this.has_cnsub}">全部</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"0" === type_value ? "is-info" : ""}" href="/?handleTop=1&type=video_type&type_value=0&has_cnsub=${this.has_cnsub}">有码</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"1" === type_value ? "is-info" : ""}" href="/?handleTop=1&type=video_type&type_value=1&has_cnsub=${this.has_cnsub}">无码</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"2" === type_value ? "is-info" : ""}" href="/?handleTop=1&type=video_type&type_value=2&has_cnsub=${this.has_cnsub}">欧美</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"3" === type_value ? "is-info" : ""}" href="/?handleTop=1&type=video_type&type_value=3&has_cnsub=${this.has_cnsub}">Fc2</a>\n \n <a style="padding:18px 18px !important;margin-left: 50px" class="button is-small ${"1" === this.has_cnsub ? "is-info" : ""}" data-cnsub-value="1">含中字磁鏈</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"0" === this.has_cnsub ? "is-info" : ""}" data-cnsub-value="0">无字幕</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-cnsub-value="">重置</a>\n </div>\n \n <div class="buttons has-addons" id="conditionBox">\n ${yearHtml}\n </div>\n </div>\n `; $(".toolbar").html(conditionHtml); $("a[data-cnsub-value]").on("click", (event2 => { const cnsubValue = $(event2.currentTarget).data("cnsub-value"); this.has_cnsub = cnsubValue.toString(); $("a[data-cnsub-value]").removeClass("is-info"); $(event2.currentTarget).addClass("is-info"); $(".toolbar a.button").not("[data-cnsub-value]").each(((index, element) => { const $link = $(element), url = new URL($link.attr("href"), window.location.origin); url.searchParams.set("has_cnsub", cnsubValue); $link.attr("href", url.toString()); })); const newUrl = new URL(window.location.href); newUrl.searchParams.set("has_cnsub", cnsubValue); history.pushState({}, "", newUrl.toString()); if ("1" === this.has_cnsub) { $(".item:contains('含中字磁鏈')").show(); $(".item:contains('含磁鏈')").hide(); } else if ("0" === this.has_cnsub) { $(".item:contains('含中字磁鏈')").hide(); $(".item:contains('含磁鏈')").show(); } else { $(".item:contains('含中字磁鏈')").show(); $(".item:contains('含磁鏈')").show(); } this.getBean("hitShowPlugin").loadScore(this.movies); })); } async checkLogin(event2, urlParams) { if (!(await storageManager.getItem("appAuthorization"))) { show.error("未登录手机端接口, 无法查看"); this.openLoginDialog(); return; } let type = "all", type_value = "", t = urlParams.get("t") || ""; if (/^y\d+$/.test(t)) { type = "year"; type_value = t.substring(1); } else if ("" !== t) { type = "video_type"; type_value = t; } let url = `/?handleTop=1&type=${type}&type_value=${type_value}`; event2 && (event2.ctrlKey || event2.metaKey) ? window.open(url, "_blank") : window.location.href = url; } openLoginDialog() { layer.open({ type: 1, title: "JavDB", closeBtn: 1, area: [ "360px", "auto" ], shadeClose: !1, content: '\n <div style="padding: 30px; font-family: \'Helvetica Neue\', Arial, sans-serif;">\n <div style="margin-bottom: 25px;">\n <input type="text" id="username" name="username" \n style="width: 100%; padding: 12px 15px; border: 1px solid #e0e0e0; border-radius: 4px; \n box-sizing: border-box; transition: all 0.3s; font-size: 14px;\n background: #f9f9f9; color: #333;"\n placeholder="用户名 | 邮箱"\n onfocus="this.style.borderColor=\'#4a8bfc\'; this.style.background=\'#fff\'"\n onblur="this.style.borderColor=\'#e0e0e0\'; this.style.background=\'#f9f9f9\'">\n </div>\n \n <div style="margin-bottom: 15px;">\n <input type="password" id="password" name="password" \n style="width: 100%; padding: 12px 15px; border: 1px solid #e0e0e0; border-radius: 4px; \n box-sizing: border-box; transition: all 0.3s; font-size: 14px;\n background: #f9f9f9; color: #333;"\n placeholder="密码"\n onfocus="this.style.borderColor=\'#4a8bfc\'; this.style.background=\'#fff\'"\n onblur="this.style.borderColor=\'#e0e0e0\'; this.style.background=\'#f9f9f9\'">\n </div>\n \n <button id="loginBtn" \n style="width: 100%; padding: 12px; background: #4a8bfc; color: white; \n border: none; border-radius: 4px; font-size: 15px; cursor: pointer;\n transition: background 0.3s;"\n onmouseover="this.style.background=\'#3a7be0\'"\n onmouseout="this.style.background=\'#4a8bfc\'">\n 登录\n </button>\n </div>\n ', success: (layero, index) => { $("#loginBtn").click((function() { const username = $("#username").val(), password = $("#password").val(); if (!username || !password) { show.error("请输入用户名和密码"); return; } let loadObj = loading(); (async (username, password) => { let url = `${apiUrl}//v1/sessions?username=${username}&password=${password}&device_uuid=04b9534d-5118-53de-9f87-2ddded77111e&device_name=iPhone&device_model=iPhone&platform=ios&system_version=17.4&app_version=official&app_version_number=1.9.29&app_channel=official`, headers = { "user-agent": "Dart/3.5 (dart:io)", "accept-language": "zh-TW", "content-type": "multipart/form-data; boundary=--dio-boundary-2210433284", jdsignature: await buildSignature() }; return await gmHttp.post(url, null, headers); })(username, password).then((async res => { let success = res.success; if (0 === success) show.error(res.message); else { if (1 !== success) { console.error("登录失败", res); throw new Error(res.message); } { let token = res.data.token; await storageManager.setItem("appAuthorization", token); await storageManager.setItem("appUser", res.data); show.ok("登录成功"); layer.close(index); window.location.href = "/?handleTop=1&period=daily"; } } })).catch((err => { console.error("登录异常:", err); show.error(err.message); })).finally((() => { loadObj.close(); })); })); } }); } } class NavBarPlugin extends BasePlugin { handle() { this.margeNav(); this.hookSearch(); if (window.location.href.includes("/search?q")) { let q = new URLSearchParams(window.location.search).get("q"); $("#search-keyword").val(q); } } hookSearch() { $("#navbar-menu-hero").after('\n <div class="navbar-menu">\n <div class="navbar-start" style="display: flex; align-items: center; gap: 5px;">\n <select id="search-type" style="padding: 8px 12px; border: 1px solid #555; border-radius: 4px; background-color: #333; color: #eee; font-size: 14px; outline: none;">\n <option value="all">影片</option>\n <option value="actor">演員</option>\n <option value="series">系列</option>\n <option value="maker">片商</option>\n <option value="director">導演</option>\n <option value="code">番號</option>\n <option value="list">清單</option>\n </select>\n <input id="search-keyword" type="text" placeholder="輸入影片番號,演員名等關鍵字進行檢索" style="padding: 8px 12px; border: 1px solid #555; border-radius: 4px; flex-grow: 1; font-size: 14px; background-color: #333; color: #eee; outline: none;">\n <a href="/advanced_search?noFold=1" title="進階檢索" style="padding: 6px 12px; background-color: #444; border-radius: 4px; text-decoration: none; color: #ddd; font-size: 14px; border: 1px solid #555;"><span>...</span></a>\n <a id="search-img-btn" style="padding: 6px 16px; background-color: #444; color: #fff; border-radius: 4px; text-decoration: none; font-weight: 500; cursor: pointer; border: 1px solid #555;">识图</a>\n <a id="search-btn" style="padding: 6px 16px; background-color: #444; color: #fff; border-radius: 4px; text-decoration: none; font-weight: 500; cursor: pointer; border: 1px solid #555;">檢索</a>\n </div>\n </div>\n '); $("#search-keyword").on("paste", (event2 => { setTimeout((() => { $("#search-btn").click(); }), 0); })).on("keypress", (event2 => { "Enter" === event2.key && setTimeout((() => { $("#search-btn").click(); }), 0); })); $("#search-btn").on("click", (event2 => { let keyword = $("#search-keyword").val(), searchCurrentType = $("#search-type option:selected").val(); "" !== keyword && (window.location.href.includes("/search?q") ? window.location.href = "/search?q=" + keyword + "&f=" + searchCurrentType : window.open("/search?q=" + keyword + "&f=" + searchCurrentType)); })); $("#search-img-btn").on("click", (() => { this.getBean("SearchByImagePlugin").open(); })); } margeNav() { $('a[href*="/feedbacks/new"]').remove(); $('a[href*="theporndude.com"]').remove(); $('a.navbar-link[href="/makers"]').parent().after('\n <div class="navbar-item has-dropdown is-hoverable">\n <a class="navbar-link">其它</a>\n <div class="navbar-dropdown is-boxed">\n <a class="navbar-item" href="/feedbacks/new" target="_blank" >反饋</a>\n <a class="navbar-item" rel="nofollow noopener" target="_blank" href="https://theporndude.com/zh">ThePornDude</a>\n </div>\n </div>\n '); } } class OtherSitePlugin extends BasePlugin { handle() { let carNum = this.getPageInfo().carNum, html = `\n <div style="margin-top:20px;margin-left: -16px;">\n <div style="display: flex;gap: 5px">\n <a id="javTrailersBtn" class="menu-btn" style="background:linear-gradient(to right, #d7ab91, rgb(255,76,76))"><span>JavTrailers</span></a>\n <a href="https://jable.tv/videos/${carNum}/" target="_blank" class="menu-btn" style="background:linear-gradient(to right, rgb(255,161,0), rgb(0,119,172))"><span>Jable</span></a>\n <a href="https://missav.ws/search/${carNum}" target="_blank" class="menu-btn" style="background:linear-gradient(to right, #d29494, rgb(254,98,142))"><span>MissAv</span></a>\n <a href="https://www.av.gl/vod/search.html?wd=${carNum}" target="_blank" class="menu-btn" style="color:#f40 !important;background:linear-gradient(to bottom, rgb(238,164,238), #fff)"><span>Avgle</span></a>\n </div>\n </div>\n `; $(".column-video-cover").append(html); $("#javTrailersBtn").on("click", (event2 => utils.openPage(`https://javtrailers.com/video/${carNum.toLowerCase().replace("-", "00")}?handle=1`, carNum, !1, event2))); } } class BusDetailPagePlugin extends BasePlugin { async initCss() { if (!window.isDetailPage) return ""; $("h4:contains('論壇熱帖')").hide(); $("h4:contains('同類影片')").hide(); $("h4:contains('推薦')").hide(); return window.location.href.includes("hideNav=1") ? "\n .navbar-default {\n display: none !important;\n }\n body {\n padding-top:0px!important;\n }\n " : ""; } async handle() { if (window.location.href.includes("/star/")) { const $avatarBox = $(".avatar-box"); if ($avatarBox.length > 0) { let parent2 = $avatarBox.parent(); parent2.css("position", "initial"); parent2.insertBefore(parent2.parent()); } } } } class DetailPageButtonPlugin extends BasePlugin { constructor() { super(); this.answerCount = 1; } handle() { this.bindHotkey(); window.isDetailPage && this.createMenuBtn(); } createMenuBtn() { const pageInfo = this.getPageInfo(), carNum = pageInfo.carNum, buttonsHtml = '\n <div style="margin: 10px auto;">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333">\n <span>屏蔽(a)</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc">\n <span>收藏(s)</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b">\n <span>加入已下载</span>\n </a>\n <a id="enable-magnets-filter" class="menu-btn" style="background-color:#c2bd4c">\n <span id="magnets-span">关闭磁力过滤</span>\n </a>\n \n\n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n </div>\n '; isJavDb && $(".tabs").after(buttonsHtml); isJavBus && $("#mag-submit-show").before(buttonsHtml); $("#favoriteBtn").on("click", (() => this.favoriteOne())); $("#filterBtn").on("click", (event2 => this.filterOne(event2))); $("#hasDownBtn").on("click", (async () => { await storageManager.saveCar(pageInfo.carNum, pageInfo.url, pageInfo.actress, Status_HAS_DOWN); window.refresh(); show.ok("操作成功", { duration: 100, callback: () => { utils.closePage(); } }); })); $("#enable-magnets-filter").on("click", (event2 => { let $span = $("#magnets-span"); const highlightMagnetPlugin = this.getBean("HighlightMagnetPlugin"); if ("关闭磁力过滤" === $span.text()) { highlightMagnetPlugin.showAll(); $span.text("开启磁力过滤"); } else { highlightMagnetPlugin.handle(); $span.text("关闭磁力过滤"); } })); $("#search-subtitle-btn").on("click", (event2 => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum}`, carNum, !1, event2))); $("#xunLeiSubtitleBtn").on("click", (() => this.searchXunLeiSubtitle(carNum))); this.showStatus(carNum).then(); } async showStatus(carNum) { let car = await storageManager.getCar(carNum); if (car) switch (car.status) { case Status_FILTER: $("#filterBtn").text("已屏蔽(a)"); break; case Status_FAVORITE: $("#favoriteBtn").text("已收藏(s)"); break; case Status_HAS_DOWN: $("#hasDownBtn").text("已加入已下载"); } } async favoriteOne() { let pageInfo = this.getPageInfo(); await storageManager.saveCar(pageInfo.carNum, pageInfo.url, pageInfo.actress, Status_FAVORITE); window.refresh(); show.ok("操作成功", { duration: 100, callback: () => { utils.closePage(); } }); } searchXunLeiSubtitle(carNum) { let loadObj = loading(); gmHttp.get(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?gcid=&cid=&name=${carNum}`).then((res => { let dataList = res.data; dataList && 0 !== dataList.length ? layer.open({ type: 1, title: "迅雷字幕", content: '<div id="table-container"></div>', area: [ "50%", "70%" ], success: layero => { new TableGenerator({ containerId: "table-container", columns: [ { key: "name", title: "文件名" }, { key: "ext", title: "类型" }, { key: "extra_name", title: "来源" } ], data: dataList, buttons: [ { text: "下载", class: "a-primary", onClick: item => { gmHttp.get(item.url).then((content => { utils.download(content, carNum + "." + item.ext); })); } } ] }); } }) : show.error("迅雷中找不到相关字幕!"); })).finally((() => { loadObj.close(); })); } async filterOne(event2, noAlert) { event2 && event2.preventDefault(); let pageInfo = this.getPageInfo(); if (noAlert) { await storageManager.saveCar(pageInfo.carNum, pageInfo.url, pageInfo.actress, Status_FILTER); window.refresh(); show.ok("操作成功", { duration: 100, callback: () => { utils.closePage(); } }); } else utils.q(event2, `是否屏蔽${pageInfo.carNum}?`, (async () => { await storageManager.saveCar(pageInfo.carNum, pageInfo.url, pageInfo.actress, Status_FILTER); window.refresh(); show.ok("操作成功", { duration: 100, callback: () => { utils.closePage(); } }); })); } speedVideo() { if ($("#preview-video").is(":visible")) { const videoEl = document.getElementById("preview-video"); if (videoEl) { videoEl.muted = !1; videoEl.currentTime += 5; } return; } const iframe = $('iframe[id^="layui-layer-iframe"]'); if (iframe.length > 0) { iframe[0].contentWindow.postMessage("speedVideo", "*"); return; } let $videoPlayBtn = $(".preview-video-container"); if ($videoPlayBtn.length > 0) { $videoPlayBtn[0].click(); const videoEl = document.getElementById("preview-video"); if (videoEl) { videoEl.currentTime += 5; videoEl.muted = !1; } } else $("#javTrailersBtn").click(); } bindHotkey() { const handlers = { a: () => { this.answerCount >= 2 ? this.filterOne(null, !0) : this.filterOne(null); this.answerCount++; }, s: () => this.favoriteOne(null), z: () => this.speedVideo() }, registerHotkey = (key, handler) => { HotkeyManager.registerHotkey(key, (() => { window.isDetailPage ? handler() : (message => { const childIframe = $(".layui-layer-content iframe"); if (0 === childIframe.length) return !1; childIframe[0].contentWindow.postMessage(message, "*"); })(key); })); }; window.isDetailPage && window.addEventListener("message", (event2 => { handlers[event2.data] && handlers[event2.data](); })); Object.entries(handlers).forEach((([key, handler]) => { registerHotkey(key, handler); })); } } class HistoryPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "dataType", "all"); __publicField(this, "tableObj", null); } handle() { isJavDb && $(".navbar-end").prepend('<div class="navbar-item has-dropdown is-hoverable">\n <a id="historyBtn" class="navbar-link nav-btn" style="color: #aade66 !important;padding-right:15px !important;">\n 历史列表\n </a>\n </div>'); isJavBus && $("#navbar").append('\n <ul class="nav navbar-nav navbar-right" style="margin-right: 10px">\n <li><a id="historyBtn" style="color: #86e114 !important;padding-right:15px !important;" role="button">历史列表</a></li>\n </ul>\n '); $("#historyBtn").on("click", (event2 => this.openHistory())); } openHistory() { layer.open({ type: 1, title: "历史列表", content: '\n <div style="margin: 10px">\n <a class="menu-btn history-btn" data-action="all" style="background-color:#d3c8a5">所有</a>\n <a class="menu-btn history-btn" data-action="filter" style="background-color:#ec4949">已屏蔽</a>\n <a class="menu-btn history-btn" data-action="favorite" style="background-color:#50adb9;">已收藏</a>\n <a class="menu-btn history-btn" data-action="hasDown" style="background-color:#8ebd6e;">已下载</a>\n </div>\n <div id="table-container"></div>\n ', area: [ "60%", "80%" ], success: async layero => { const dataList = await this.getDataList(); this.loadTableData(dataList); $(".layui-layer-content").on("click", ".history-btn", (async event2 => { this.dataType = $(event2.target).data("action"); this.reloadTable(); })); }, end: async () => window.refresh() }); } async handleClickDetail(event2, data) { if (isJavDb) if (data.carNum.includes("FC2-")) { const parts = data.url.split("/"), movieId = parts[parts.length - 1].split("#")[0]; this.getBean("fc2Plugin").openFc2Page(movieId, data.carNum, data.url); } else utils.openPage(data.url, data.carNum, !1, event2); if (isJavBus) { let url = data.url; if (url.includes("javdb")) if (data.carNum.includes("FC2-")) { const parts = url.split("/"), movieId = parts[parts.length - 1].split("#")[0]; let openUrl = `${await storageManager.getSetting("javDbUrl", "https://javdb.com")}/users/collection_codes?movieId=${movieId}&carNum=${data.carNum}&url=${url}`; window.open(openUrl, "_blank"); } else window.open(url, "_blank"); else utils.openPage(data.url, data.carNum, !1, event2); } } async reloadTable() { const dataList = await this.getDataList(); this.tableObj.update(dataList); } handleDelete(event2, data) { utils.q(event2, `是否移除${data.carNum}?`, (async () => { await storageManager.removeCar(data.carNum); this.getBean("listPagePlugin").showCarNumBox(data.carNum); this.reloadTable().then(); })); } async getDataList() { let dataList = await storageManager.getCarList(); this.allCount = dataList.length; this.filterCount = 0; this.favoriteCount = 0; this.hasDownCount = 0; dataList.forEach((item => { switch (item.status) { case Status_FILTER: this.filterCount++; break; case Status_FAVORITE: this.favoriteCount++; break; case Status_HAS_DOWN: this.hasDownCount++; } })); $('a[data-action="all"]').text(`所有 (${this.allCount})`); $('a[data-action="filter"]').text(`已屏蔽 (${this.filterCount})`); $('a[data-action="favorite"]').text(`已收藏 (${this.favoriteCount})`); $('a[data-action="hasDown"]').text(`已下载 (${this.hasDownCount})`); return "all" === this.dataType ? dataList : dataList.filter((item => item.status === this.dataType)); } loadTableData(dataList) { this.tableObj = new TableGenerator({ containerId: "table-container", columns: [ { key: "carNum", title: "番号" }, { key: "actress", title: "演员", width: "250px" }, { key: "createDate", title: "操作日期", width: "185px" }, { key: "url", title: "来源", render: item => { let url = item.url; return url.includes("javdb") ? '<span style="color:#d34f9e">Javdb</span>' : url.includes("javbus") ? '<span style="color:#eaa813">JavBus</span>' : `<span style="color:#050505">${url}</span>`; } }, { key: "status", title: "状态", width: "250px", render: item => { let color, text = ""; switch (item.status) { case "filter": color = "#ec4949"; text = "已屏蔽"; break; case "favorite": color = "#50adb9"; text = "已收藏"; break; case "hasDown": color = "#8ebd6e"; text = "已下载"; } return `<span style="color:${color}">${text}</span>`; } } ], data: dataList, buttons: [ { text: "移除", class: "a-danger", onClick: item => { this.handleDelete(event, item); } }, { text: "详情页", class: "a-info", onClick: item => { this.handleClickDetail(event, item); } } ] }); } } class ReviewPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "floorIndex", 1); } async handle() { if (window.isDetailPage) { if (isJavDb) { const parts = window.location.href.split("?")[0].split("/"), movieId = parts[parts.length - 1].split("#")[0]; await this.showReview(movieId); await this.getBean("RelatedPlugin").showRelated(); } if (isJavBus) { let carNum = this.getPageInfo().carNum; const movies = await (async keyword => { let url = `${apiUrl}/v2/search`, headers = { "user-agent": "Dart/3.5 (dart:io)", "accept-language": "zh-TW", host: "jdforrepam.com", jdsignature: await buildSignature() }, params = { q: keyword, page: 1, type: "movie", limit: 1, movie_type: "all", from_recent: "false", movie_filter_by: "all", movie_sort_by: "relevance" }; return (await gmHttp.get(url, params, headers)).data.movies; })(carNum); let movieId = null; for (let i = 0; i < movies.length; i++) { let item = movies[i]; if (item.number.toLowerCase() === carNum.toLowerCase()) { movieId = item.id; break; } } if (!movieId) { show.error("解析视频ID失败, 该视频可能在JavDb中不存在, 无法获取评论数据"); return; } this.showReview(movieId, $("#sample-waterfall")).then(); } } } async showReview(movieId, $eleBox) { let $magnets = $("#magnets-content"); $eleBox && ($magnets = $eleBox); $magnets.append('<div id="reviewsLoading" style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">获取评论中...</div>'); let reviewCount = await storageManager.getSetting("reviewCount", 20), dataList = null; try { dataList = await getReviews(movieId, 1, reviewCount); } catch (e) { console.error(e); } $("#reviewsLoading").remove(); $magnets.append('\n <div style=" display: flex; align-items: center; margin: 16px 0; color: #666; font-size: 14px; ">\n <span style=" flex: 1; height: 1px; background: linear-gradient(to right, transparent, #999, transparent); "></span>\n <span style="padding: 0 10px;">评论区</span>\n <a id="reviewsFold" style=" margin-left: 8px; color: #1890ff; text-decoration: none; display: flex; align-items: center; ">\n <span class="toggle-text">折叠</span>\n <span class="toggle-icon" style="margin-left: 4px;">▲</span>\n </a>\n <span style=" flex: 1; height: 1px; background: linear-gradient(to right, transparent, #999, transparent); "></span>\n </div>\n '); $("#reviewsFold").on("click", (() => { const textSpan = $("#reviewsFold .toggle-text"), iconSpan = $("#reviewsFold .toggle-icon"); if ("展开" === textSpan.text()) { textSpan.text("折叠"); iconSpan.text("▲"); $reviewsContainer.show(); $reviewsFooter.show(); } else { textSpan.text("展开"); iconSpan.text("▼"); $reviewsContainer.hide(); $reviewsFooter.hide(); } })); $magnets.append('<div id="reviewsContainer"></div>'); $magnets.append('<div id="reviewsFooter"></div>'); const $reviewsContainer = $("#reviewsContainer"), $reviewsFooter = $("#reviewsFooter"); if (!dataList) { $reviewsContainer.append('<div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">获取评论失败</div>'); return; } 0 === dataList.length && $reviewsContainer.append('<div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">无评论</div>'); const reviewKeywordList = await storageManager.getReviewFilterKeywordList(); this.displayReviews(dataList, $reviewsContainer, reviewKeywordList); if (dataList.length === reviewCount) { $reviewsFooter.html('\n <button id="loadMoreReviews" style="width:100%; background-color: #e1f5fe; border:none; padding:10px; margin-top:10px; cursor:pointer; color:#0277bd; font-weight:bold; border-radius:4px;">\n 加载更多评论\n </button>\n <div id="reviewsEnd" style="display:none; text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部评论</div>\n '); let currentPage = 1; $("#loadMoreReviews").click((async () => { $("#loadMoreReviews").text("加载中...").prop("disabled", !0); currentPage++; const moreData = await getReviews(movieId, currentPage, reviewCount); this.displayReviews(moreData, $reviewsContainer, reviewKeywordList); if (moreData.length < reviewCount) { $("#loadMoreReviews").remove(); $("#reviewsEnd").show(); } else $("#loadMoreReviews").text("加载更多评论").prop("disabled", !1); })); } else dataList.length > 0 && $reviewsFooter.html('<div style="text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部评论</div>'); } displayReviews(dataList, $container, reviewKeywordList) { if (dataList.length) { dataList.forEach((item => { let isReviewKeyword = !1; for (let i = 0; i < reviewKeywordList.length; i++) if (item.content.indexOf(reviewKeywordList[i]) > -1) { isReviewKeyword = !0; break; } if (isReviewKeyword) return; let starsHtml = ""; for (let i = 0; i < item.score; i++) starsHtml += '<i class="icon-star"></i>'; let content = item.content.replace(/(https?:\/\/[^\s]+|magnet:\?[^\s"'\u4e00-\u9fa5,。?!()【】]+)/gi, (match => `<a href="${match}" class="a-primary" \n style="padding:0; word-break: break-all; white-space: pre-wrap;" target="_blank" rel="noopener noreferrer">${match}</a>\n\x3c!-- <a class="a-success review-magnet" style="padding:0;margin-left:0">预览</a>--\x3e`)), commentHtml = `\n <div class="item columns is-desktop" style="display:block;margin-top:6px;background-color:#ffffff;padding:10px;margin-left: -10px;word-break: break-word;position:relative;">\n <span style="position:absolute;top:5px;right:10px;color:#999;font-size:12px;">#${this.floorIndex++}楼</span>\n ${item.username} <span class="score-stars">${starsHtml}</span> \n <span class="time">${item.created_at.replace("T", " ").replace(".000Z", "")}</span> \n 点赞:${item.likes_count}\n <p class="review-content" style="margin-top: 5px;"> ${content} </p>\n </div>\n `; $container.append(commentHtml); })); utils.rightClick($(".review-content"), (event2 => { const selectedText = window.getSelection().toString(); if (selectedText) { event2.preventDefault(); utils.q(event2, `是否将 '${selectedText}' 加入评论区关键词?`, (async () => { await storageManager.saveReviewFilterKeyword(selectedText); show.ok("操作成功, 刷新页面后生效"); })); } })); } } } class FilterTitleKeywordPlugin extends BasePlugin { handle() { if (!window.isDetailPage) return; let $titles, $male; if (isJavDb) { $titles = $("h2"); $male = $(".male").prev(); } isJavBus && ($titles = $("h3")); utils.rightClick($titles, (event2 => { const selectedText = window.getSelection().toString(); if (selectedText) { event2.preventDefault(); let tempEvent = { clientX: event2.clientX, clientY: event2.clientY + 80 }; utils.q(tempEvent, `是否屏蔽标题关键词 ${selectedText}?`, (async () => { await storageManager.saveTitleFilterKeyword(selectedText); window.refresh(); utils.closePage(); })); } })); $male && $male.length > 0 && utils.rightClick($male, (event2 => { event2.preventDefault(); let text = $(event2.target).text().trim(); utils.q(event2, `是否屏蔽演员${text}?`, (async () => { await storageManager.saveFilterActor(text); window.refresh(); const detailPageButtonPlugin = this.getBean("detailPageButtonPlugin"); await detailPageButtonPlugin.filterOne(null, !0); })); })); } } class ListPageButtonPlugin extends BasePlugin { handle() { window.isListPage && this.createMenuBtn(); } createMenuBtn() { if (isJavDb) { if (window.location.href.includes("/actors/")) { $(".toolbar .buttons").append('\n <a class="menu-btn" id="waitCheckBtn" \n style="background-color:#56c938 !important;; margin-left: 40px;margin-bottom: 8px; border-bottom:none !important; border-radius:3px;">\n <span>打开待鉴定</span>\n </a>\n <a class="menu-btn" id="waitDownBtn" \n style="background-color:#2caac0 !important;; margin-left: 10px;margin-bottom: 8px; border-bottom:none !important; border-radius:3px;">\n <span>打开已收藏</span>\n </a>\n '); $(".toolbar .buttons").append(`\n <a class="menu-btn" id="sort-toggle-btn" \n style="background-color:#8783ab !important; margin-left: 50px;margin-bottom: 8px; border-bottom:none !important; border-radius:3px;">当前排序方式: ${"rateCount" === localStorage.getItem("sortMethod") ? "评价人数" : "date" === localStorage.getItem("sortMethod") ? "时间" : "默认"}</a>\n `); } else { $(".tabs ul").append('\n <li class="is-active" id="waitCheckBtn">\n <a class="menu-btn" style="background-color:#56c938 !important;margin-left: 20px;border-bottom:none !important;border-radius:3px;">\n <span>打开待鉴定</span>\n </a>\n </li>\n <li class="is-active" id="waitDownBtn">\n <a class="menu-btn" style="background-color:#2caac0 !important;margin-left: 20px;border-bottom:none !important;border-radius:3px;">\n <span>打开已收藏</span>\n </a>\n </li>\n '); $(".tabs ul").after(`\n <div style="padding:10px">\n <a class="menu-btn" id="sort-toggle-btn" \n style="background-color:#8783ab !important; margin-left: 20px; border-bottom:none !important; border-radius:3px;">\n 当前排序方式: ${"rateCount" === localStorage.getItem("sortMethod") ? "评价人数" : "date" === localStorage.getItem("sortMethod") ? "时间" : "默认"}\n </a>\n </div>\n `); } this.sortItems(); } if (isJavBus) { const buttonsHtml = '\n <div style="margin-top: 10px">\n <a id="waitCheckBtn" class="menu-btn" style="background-color:#56c938 !important;margin-left: 14px;border-bottom:none !important;border-radius:3px;">\n <span>打开待鉴定</span>\n </a>\n <a id="waitDownBtn" class="menu-btn" style="background-color:#2caac0 !important;margin-left: 5px;border-bottom:none !important;border-radius:3px;">\n <span>打开已收藏</span>\n </a>\n </div>\n '; $(".masonry").parent().prepend(buttonsHtml); } $("#waitCheckBtn").on("click", (event2 => { this.openWaitCheck(event2).then(); })); $("#waitDownBtn").on("click", (event2 => { this.openFavorite(event2).then(); })); $("#sort-toggle-btn").on("click", (event2 => { const currentMethod = localStorage.getItem("sortMethod"); let newMethod; newMethod = currentMethod && "default" !== currentMethod ? "rateCount" === currentMethod ? "date" : "default" : "rateCount"; const methodText = { default: "默认", rateCount: "评价人数", date: "时间" }[newMethod]; $(event2.target).text(`当前排序方式: ${methodText}`); localStorage.setItem("sortMethod", newMethod); this.sortItems(); })); } sortItems() { const method = localStorage.getItem("sortMethod"); if (!method) return; $(".movie-list .item").each((function(index) { $(this).attr("data-original-index") || $(this).attr("data-original-index", index); })); const $container = $(".movie-list"), $items = $(".item", $container); if ("default" === method) $items.sort((function(a, b) { return $(a).data("original-index") - $(b).data("original-index"); })).appendTo($container); else { const items = $items.get(); items.sort((function(a, b) { if ("rateCount" === method) { const getScore = el => { const match = $(el).find(".score .value").text().match(/由(\d+)人/); return match ? parseFloat(match[1]) : 0; }; return getScore(b) - getScore(a); } { const getDate = el => { const dateStr = $(el).find(".meta").text().trim(); return new Date(dateStr); }; return getDate(b) - getDate(a); } })); $container.empty().append(items); } } async openWaitCheck() { let selector; isJavDb && (selector = Db); isJavBus && (selector = Bus); const maxCount = await storageManager.getSetting("waitCheckCount", 5), excludedTexts = [ "已收藏", "已屏蔽", "已下载" ]; let count = 0; $(`${selector.itemSelector}:visible`).each(((i, el) => { if (count >= maxCount) return !1; const $el = $(el); if (!excludedTexts.some((text => $el.find(`span:contains('${text}')`).length > 0))) { const $link = $el.find("a"); if ($link.length) { let href = $link.attr("href"); if (href) { href += href.includes("?") ? "&autoPlay=1" : "?autoPlay=1"; window.open(href); count++; } } } })); 0 === count && show.info("没有需鉴定的视频"); } async openFavorite() { let openCount = await storageManager.getSetting("waitCheckCount", 5); const favoriteList = (await storageManager.getCarList()).filter((item => item.status === Status_FAVORITE)); for (let i = 0; i < openCount; i++) { if (i >= favoriteList.length) return; let data = favoriteList[i], carNum = data.carNum, url = data.url; if (carNum.includes("FC2-")) { const parts = data.url.split("/"), movieId = parts[parts.length - 1].split("#")[0]; let javDbUrl = await storageManager.getSetting("javDbUrl", "https://javdb.com"); window.open(`${javDbUrl}/users/collection_codes?movieId=${movieId}&carNum=${carNum}&url=${url}`); } else window.open(url); } } } class ListPagePlugin extends BasePlugin { async handle() { this.cleanRepeatId(); this.replaceHdImg(); await this.doFilter(); this.bindClick().then(); new BroadcastChannel("channel-refresh").addEventListener("message", (async event2 => { "refresh" === event2.data.type && await this.doFilter(); })); this.checkDom(); } checkDom() { if (!window.isListPage) return; const selector = this.getSelector(), targetNode = document.querySelector(selector.boxSelector), observer = new MutationObserver((mutations => { utils.log("检查"); observer.disconnect(); try { this.replaceHdImg(); this.doFilter().then(); this.getBean("ListPageButtonPlugin").sortItems(); } finally { observer.observe(targetNode, config); } })), config = { childList: !0, subtree: !1 }; observer.observe(targetNode, config); } cleanRepeatId() { if (!isJavBus) return; $("#waterfall_h").removeAttr("id").attr("id", "no-page"); const $waterfalls = $('[id="waterfall"]'); 0 !== $waterfalls.length && $waterfalls.each((function() { const $current = $(this); if (!$current.hasClass("masonry")) { $current.children().insertAfter($current); $current.remove(); } })); } async doFilter() { if (!window.isListPage) return; let movieList = $(this.getSelector().itemSelector).toArray(); await this.filterMovieList(movieList); await this.getBean("autoPagePlugin").handlePaging(); } async filterMovieList(movieList) { const carList = await storageManager.getCarList(), filterKeywordList = await storageManager.getTitleFilterKeyword(), filterCarNums = carList.filter((item => item.status === Status_FILTER)).map((item => item.carNum)), favoriteCarNums = carList.filter((item => item.status === Status_FAVORITE)).map((item => item.carNum)), hasDownCarNums = carList.filter((item => item.status === Status_HAS_DOWN)).map((item => item.carNum)); let hideFilterItem = await storageManager.getSetting("hideFilterItem", "yes"), href = window.location.href; (href.includes("search?q") || href.includes("/search/") || href.includes("/users/")) && (hideFilterItem = "no"); movieList.forEach((ele => { let $box = $(ele); const {carNum: carNum, aHref: aHref, title: title} = this.findCarNumAndHref($box), hideKey = `${carNum}-hide`, keywordHideKey = `${carNum}-keywordHide`, tagKey = `${carNum}-tag`; if ("no" === hideFilterItem && $box.attr("data-hide") === hideKey) { $box.show(); $box.removeAttr("data-hide"); } if (filterKeywordList.some((filterKeyword => title.includes(filterKeyword) || carNum.includes(filterKeyword))) && $box.attr("data-keyword-hide") !== keywordHideKey) { $box.hide(); $box.attr("data-keyword-hide", keywordHideKey); return; } if (filterCarNums.includes(carNum) && "yes" === hideFilterItem && $box.attr("data-hide") !== hideKey) { $box.hide(); $box.attr("data-hide", hideKey); return; } if (hasDownCarNums.includes(carNum) && "yes" === hideFilterItem && $box.attr("data-hide") !== hideKey) { $box.hide(); $box.attr("data-hide", hideKey); return; } let tagText = "", color = ""; if (filterCarNums.includes(carNum)) { tagText = "已屏蔽"; color = "#d95427"; } else if (favoriteCarNums.includes(carNum)) { tagText = "已收藏"; color = "#2caac0"; } else if (hasDownCarNums.includes(carNum)) { tagText = "已下载"; color = "#58c433"; } if (tagText && $box.attr("data-tag") !== tagKey) { isJavDb && $box.find(".tags").append(`\n <span class="tag is-success" \n style="margin-right: 5px; border-radius:10px; position:absolute; right: 0; top:5px;z-index:10;background-color: ${color} !important;">\n ${tagText}\n </span>`); if (isJavBus) { let tagHtml = `<a class="a-primary" style="margin-right: 5px; padding: 0 5px;color: #fff; border-radius:10px; position:absolute; right: 0; top:5px;z-index:10;background-color: ${color} !important;"><span>${tagText}</span></a>`; $box.find(".item-tag").append(tagHtml); } $box.attr("data-tag", tagKey); } })); $("#waitDownBtn span").text(`打开已收藏 (${favoriteCarNums.length})`); } async bindClick() { let selector = this.getSelector(), dialogOpenDetail = await storageManager.getSetting("dialogOpenDetail", "yes"); $(selector.boxSelector).on("click", ".item img", (event2 => { event2.preventDefault(); if ($(event2.target).closest("div.meta-buttons").length) return; const $box = $(event2.target).closest(".item"), {carNum: carNum, aHref: aHref} = this.findCarNumAndHref($box); if (carNum.includes("FC2-")) { let movieId = aHref.split("/").filter(Boolean).pop(); this.getBean("fc2Plugin").openFc2Page(movieId, carNum, aHref); } else "yes" === dialogOpenDetail ? utils.openPage(aHref, carNum, !1, event2) : window.open(aHref); })); $(selector.boxSelector).on("contextmenu", ".item img", (event2 => { event2.preventDefault(); const $box = $(event2.target).closest(".item"), {carNum: carNum, aHref: aHref} = this.findCarNumAndHref($box); utils.q(event2, `是否屏蔽番号 ${carNum}?`, (async () => { await storageManager.saveCar(carNum, aHref, "", Status_FILTER); window.refresh(); show.ok("操作成功"); })); })); } findCarNumAndHref($box) { let carNum, title, aHref = $box.find("a").attr("href"); if (isJavDb) { carNum = $box.find(".video-title").find("strong").text(); title = $box.find(".video-title").text(); } if (isJavBus) { carNum = aHref.split("/").filter(Boolean).pop(); title = $box.find("img").attr("title"); } return { carNum: carNum, aHref: aHref, title: title }; } showCarNumBox(matchCarNum) { const matchingBox = $(".movie-list .item").toArray().find((item => $(item).find(".video-title strong").text() === matchCarNum)); if (matchingBox) { const $matchingBox = $(matchingBox); if ($matchingBox.attr("data-hide") === `${matchCarNum}-hide`) { $matchingBox.show(); $matchingBox.removeAttr("data-hide"); } } } replaceHdImg(coverImgNodeList) { coverImgNodeList || (coverImgNodeList = document.querySelectorAll(this.getSelector().coverImgSelector)); isJavDb && coverImgNodeList.forEach((img => { img.src = img.src.replace("thumbs", "covers"); })); if (isJavBus) { const THUMB_PATH_REGEX = /\/(imgs|pics)\/(thumb|thumbs)\//, IMG_EXT_REGEX = /(\.jpg|\.jpeg|\.png)$/i, replaceWithHd = img => { if (img.src && THUMB_PATH_REGEX.test(img.src) && "true" !== img.dataset.hdReplaced) { img.src = img.src.replace(THUMB_PATH_REGEX, "/$1/cover/").replace(IMG_EXT_REGEX, "_b$1"); img.dataset.hdReplaced = "true"; img.loading = "lazy"; } }; coverImgNodeList.forEach((img => { replaceWithHd(img); })); } } } class AutoPagePlugin extends BasePlugin { constructor() { super(); this.paging = !1; this.selector = null; isJavDb && (this.selector = Db); isJavBus && (this.selector = Bus); } async handle() { window.isListPage && !this.shouldDisablePaging() && this.bindPageClick().then(); } async bindPageClick() { $(".pagination-link, .pagination-next, .pagination-previous, .pagination li a").on("click", (event2 => { event2.preventDefault(); event2.stopPropagation(); let href = $(event2.target).attr("href"); this.parsePage(href).then(); })); 0 === $("#auto-page").length && await this.insertPageBtn(); $("#auto-page").on("click", (async event2 => { event2.preventDefault(); if ("yes" === await storageManager.getItem(storageManager.auto_page_key)) { await storageManager.setItem(storageManager.auto_page_key, "no"); $("#auto-page").html("开启自动翻页"); } else { await storageManager.setItem(storageManager.auto_page_key, "yes"); $("#auto-page").html("关闭自动翻页"); await this.handlePaging(); } })); } async insertPageBtn() { let initText = "yes" === await storageManager.getItem(storageManager.auto_page_key) ? "关闭自动翻页" : "开启自动翻页"; isJavDb && $(".pagination").prepend(`<a style="background-color:#fff; order: 2;padding: calc(.5em - 1px) .75em;" id='auto-page'>${initText}</a>`); isJavBus && $(".pagination").append(`<li><a style="margin-left: 20px;cursor: pointer;" id='auto-page'>${initText}</a></li>`); } async parsePage(href) { let loadObj = loading(); try { const html = await http.get(href), dom = (new DOMParser).parseFromString(html, "text/html"); let itemList = dom.querySelectorAll(this.selector.requestDomItemSelector), pagination = dom.querySelectorAll(".pagination"); const listPagePlugin = this.getBean("listPagePlugin"); await listPagePlugin.filterMovieList(itemList); let coverImgNodeList = dom.querySelectorAll(this.selector.coverImgSelector); listPagePlugin.replaceHdImg(coverImgNodeList); let $movieList = $(this.selector.boxSelector); $movieList.fadeOut(300, (() => { $movieList.html(itemList).fadeIn(300, (async () => {})); })); await this.insertPageBtn(); $(".pagination").replaceWith(pagination); window.history.pushState({}, "", href); if (isJavBus) { const pageNumber = this.getPageNumberFromUrl(href); document.title = document.title.replace(/第\d+頁/, "第" + pageNumber + "頁"); } await utils.smoothScrollToTop(); await this.bindPageClick(); await this.handlePaging(); } finally { loadObj.close(); } } getPageNumberFromUrl(url) { const match = url.match(/\/page\/(\d+)/); return match ? parseInt(match[1], 10) : null; } shouldDisablePaging() { return [ "search?q", "handlePlayback=1", "handleTop=1", "/want_watch_videos", "/watched_videos" ].some((path => window.location.href.includes(path))); } async handlePaging() { if (this.shouldDisablePaging()) return; if ($("#no-page").length) { $("#auto-page").remove(); return; } if (0 === $(this.selector.boxSelector).length) return; if (!window.isListPage) return; if (this.paging) return; let needPaging = !0; $(`${this.selector.itemSelector}:visible`).each(((i, el) => { 0 === $(el).find("span:contains('已收藏')").length && 0 === $(el).find("span:contains('已屏蔽')").length && 0 === $(el).find("span:contains('已下载')").length && (needPaging = !1); })); if (!needPaging) return; if ("yes" !== await storageManager.getItem(storageManager.auto_page_key)) return; let nextBtn = null; isJavDb && (nextBtn = $(".pagination-next")); isJavBus && (nextBtn = $("#next")); if (nextBtn && 0 !== nextBtn.length) { this.paging = !0; show.info("下一页....", { duration: 500, callback: () => { nextBtn[0].click(); this.paging = !1; } }); } } } class HighlightMagnetPlugin extends BasePlugin { handle() { this.handleDb(); this.handleBus(); } handleDb() { if (!isJavDb || !isDetailPage) return; let magnetNameList = $("#magnets-content .name").toArray(), has4k_C_UC = !1; magnetNameList.forEach((el => { let item = $(el), text = item.text().toLowerCase(); text.indexOf("4k") > -1 && item.css("color", "#f40"); (text.indexOf("-c") > -1 || text.indexOf("-uc") > -1 || text.indexOf("4k") > -1) && (has4k_C_UC = !0); })); has4k_C_UC ? magnetNameList.forEach((el => { let item = $(el), text = item.text().toLowerCase(); text.indexOf("-c") > -1 || text.indexOf("-uc") > -1 || text.indexOf("4k") > -1 || item.parent().parent().parent().hide(); })) : $("#enable-magnets-filter").addClass("do-hide"); } handleBus() { isJavBus && isDetailPage && utils.loopDetector((() => $("#magnet-table td a").length > 0), (() => { let magnetNameList = $("#magnet-table tr td:first-child a:first-child").toArray(), has4k_C_UC = !1; magnetNameList.forEach((el => { let item = $(el), text = item.text().toLowerCase(); text.indexOf("4k") > -1 && item.css("color", "#f40"); (text.indexOf("-c") > -1 || text.indexOf("-uc") > -1 || text.indexOf("4k") > -1) && (has4k_C_UC = !0); })); has4k_C_UC ? magnetNameList.forEach((el => { let item = $(el), text = item.text().toLowerCase(); text.indexOf("-c") > -1 || text.indexOf("-uc") > -1 || text.indexOf("4k") > -1 || item.parent().parent().hide(); })) : $("#enable-magnets-filter").addClass("do-hide"); })); } showAll() { if (isJavDb) { $("#magnets-content .item").toArray().forEach((el => $(el).show())); } isJavBus && $("#magnet-table tr").toArray().forEach((el => $(el).show())); } } class AliyunApi { constructor(refresh_token) { this.baseApiUrl = "https://api.aliyundrive.com"; this.refresh_token = refresh_token; this.authorization = null; this.default_drive_id = null; this.backupFolderId = null; } async getDefaultDriveId() { if (this.default_drive_id) return this.default_drive_id; this.userInfo = await this.getUserInfo(); this.default_drive_id = this.userInfo.default_drive_id; return this.default_drive_id; } async getHeaders() { if (this.authorization) return { authorization: this.authorization }; this.authorization = await this.getAuthorization(); return { authorization: this.authorization }; } async getAuthorization() { let url = this.baseApiUrl + "/v2/account/token", data = { refresh_token: this.refresh_token, grant_type: "refresh_token" }; try { return "Bearer " + (await http.post(url, data)).access_token; } catch (e) { throw e.message.includes("is not valid") ? new Error("refresh_token无效, 请重新填写并保存") : e; } } async getUserInfo() { const headers = await this.getHeaders(); let url = this.baseApiUrl + "/v2/user/get"; return await http.post(url, {}, headers); } async deleteFile(file_id, drive_id = null) { if (!file_id) throw new Error("未传入file_id"); drive_id || (drive_id = await this.getDefaultDriveId()); let data = { file_id: file_id, drive_id: drive_id }, url = this.baseApiUrl + "/v2/recyclebin/trash"; const headers = await this.getHeaders(); await gmHttp.post(url, data, headers); return {}; } async createFolder(name, drive_id = null, parent_folder_id = "root") { drive_id || (drive_id = await this.getDefaultDriveId()); let url = this.baseApiUrl + "/adrive/v2/file/createWithFolders", data = { name: name, type: "folder", parent_file_id: parent_folder_id, check_name_mode: "auto_rename", content_hash_name: "sha1", drive_id: drive_id }; const headers = await this.getHeaders(), result = await gmHttp.post(url, data, headers); return JSON.parse(result); } async getFileList(parent_folder_id = "root", drive_id = null) { drive_id || (drive_id = await this.getDefaultDriveId()); let url = this.baseApiUrl + "/adrive/v3/file/list"; const data = { drive_id: drive_id, parent_file_id: parent_folder_id, limit: 200, all: !1, url_expire_sec: 14400, image_thumbnail_process: "image/resize,w_256/format,avif", image_url_process: "image/resize,w_1920/format,avif", video_thumbnail_process: "video/snapshot,t_120000,f_jpg,m_lfit,w_256,ar_auto,m_fast", fields: "*", order_by: "updated_at", order_direction: "DESC" }, headers = await this.getHeaders(); return (await gmHttp.post(url, data, headers)).items; } async uploadFile(folder_id, fileName, uploadContent, drive_id = null) { let createFileUrl = this.baseApiUrl + "/adrive/v2/file/createWithFolders"; drive_id || (drive_id = await this.getDefaultDriveId()); let data = { drive_id: drive_id, part_info_list: [ { part_number: 1 } ], parent_file_id: folder_id, name: fileName, type: "file", check_name_mode: "auto_rename" }; const headers = await this.getHeaders(), createFileResult = await gmHttp.post(createFileUrl, data, headers), upload_id = createFileResult.upload_id, upload_file_id = createFileResult.file_id, upload_url = createFileResult.part_info_list[0].upload_url; console.log("创建完成: ", createFileResult); await this._doUpload(upload_url, uploadContent); const completeResult = await gmHttp.post("https://api.aliyundrive.com/v2/file/complete", data = { drive_id: "745851", file_id: upload_file_id, upload_id: upload_id }, headers); console.log("标记完成:", completeResult); } _doUpload(upload_url, uploadContent) { return new Promise(((resolve, reject) => { $.ajax({ type: "PUT", url: upload_url, data: uploadContent, contentType: " ", processData: !1, success: (res, status, xhr) => { if (200 === xhr.status) { console.log("上传成功:", res); resolve({}); } else reject(xhr); }, error: xhr => { console.error("上传失败", xhr.responseText); reject(xhr); } }); })); } async getDownloadUrl(file_id, drive_id = null) { drive_id || (drive_id = await this.getDefaultDriveId()); let url = this.baseApiUrl + "/v2/file/get_download_url"; const headers = await this.getHeaders(); let data = { file_id: file_id, drive_id: drive_id }; return (await gmHttp.post(url, data, headers)).url; } async _createBackupFolder(folderName) { const fileList = await this.getFileList(); let folderObj = null; for (let i = 0; i < fileList.length; i++) { let file = fileList[i]; if (file.name === folderName) { folderObj = file; break; } } if (!folderObj) { console.log("不存在目录, 进行创建"); folderObj = await this.createFolder(folderName); } this.backupFolderId = folderObj.file_id; } async backup(folderName, fileName, uploadContent) { if (this.backupFolderId) await this.uploadFile(this.backupFolderId, fileName, uploadContent); else { await this._createBackupFolder(folderName); await this.uploadFile(this.backupFolderId, fileName, uploadContent); } } async getBackupList(folderName) { let dataList = null; if (this.backupFolderId) dataList = await this.getFileList(this.backupFolderId); else { await this._createBackupFolder(folderName); dataList = await this.getFileList(this.backupFolderId); } const fileList = []; dataList.forEach((data => { fileList.push({ name: data.name, fileId: data.file_id, createTime: data.created_at, size: data.size }); })); return fileList; } } class WebDavApi { constructor(davUrl, username, password) { this.davUrl = davUrl.endsWith("/") ? davUrl : davUrl + "/"; this.username = username; this.password = password; this.folderName = null; } _getAuthHeaders() { return { Authorization: `Basic ${btoa(`${this.username}:${this.password}`)}`, Depth: "1" }; } _sendRequest(method, path, headers = {}, data) { return new Promise(((resolve, reject) => { const url = this.davUrl + path, allHeaders = { ...this._getAuthHeaders(), ...headers }; GM_xmlhttpRequest({ method: method, url: url, headers: allHeaders, data: data, onload: response => { response.status >= 200 && response.status < 300 ? resolve(response) : reject(new Error(`Request failed with status ${response.status}: ${response.statusText}`)); }, onerror: response => { console.error("请求WebDav发生错误:", response); reject(new Error("请求WebDav失败, 请检查服务是否启动, 凭证是否正确")); } }); })); } async backup(folderName, fileName, uploadContent) { await this._sendRequest("MKCOL", folderName); const path = folderName + "/" + fileName; await this._sendRequest("PUT", path, { "Content-Type": "text/plain" }, uploadContent); } async getFileList(folderName) { var _a, _b; const xmlResponse = (await this._sendRequest("PROPFIND", folderName, { "Content-Type": "application/xml" }, '\n <?xml version="1.0"?>\n <d:propfind xmlns:d="DAV:">\n <d:prop>\n <d:displayname />\n <d:getcontentlength />\n <d:creationdate />\n <d:iscollection />\n </d:prop>\n </d:propfind>\n ')).responseText, items = (new DOMParser).parseFromString(xmlResponse, "text/xml").getElementsByTagNameNS("DAV:", "response"), fileList = []; for (let i = 0; i < items.length; i++) { if (0 === i) continue; if ("1" === items[i].getElementsByTagNameNS("DAV:", "iscollection")[0].textContent) continue; const name = items[i].getElementsByTagNameNS("DAV:", "displayname")[0].textContent, size = (null == (_a = items[i].getElementsByTagNameNS("DAV:", "getcontentlength")[0]) ? void 0 : _a.textContent) || "0", createTime = (null == (_b = items[i].getElementsByTagNameNS("DAV:", "creationdate")[0]) ? void 0 : _b.textContent) || ""; fileList.push({ fileId: name, name: name, size: size, createTime: createTime }); } fileList.reverse(); return fileList; } async deleteFile(fileId) { let path = this.folderName + "/" + encodeURI(fileId); await this._sendRequest("DELETE", path, { "Cache-Control": "no-cache" }); } async getBackupList(folderName) { this.folderName = folderName; await this._sendRequest("MKCOL", folderName); return this.getFileList(folderName); } async getFileContent(filePath) { let path = this.folderName + "/" + filePath; return (await this._sendRequest("GET", path, { Accept: "application/octet-stream" })).responseText; } } class SettingPlugin extends BasePlugin { constructor() { super(...arguments); __privateAdd(this, _SettingPlugin_instances); __publicField(this, "folderName", "JSH-数据备份"); } async initCss() { let containerWidth = await storageManager.getSetting("containerWidth", "100"), containerColumns = await storageManager.getSetting("containerColumns", 5), containerWidthCss = `\n section .container{\n max-width: 1000px !important;\n min-width: ${containerWidth}%;\n }\n .movie-list{\n grid-template-columns: repeat(${containerColumns}, minmax(0, 1fr));\n }\n `; isJavBus && (containerWidthCss = `\n .container-fluid .row{\n max-width: 1000px !important;\n min-width: ${containerWidth}%;\n margin: auto auto;\n }\n \n .masonry {\n grid-template-columns: repeat(${containerColumns}, minmax(0, 1fr));\n }\n `); return `\n <style>\n ${containerWidthCss}\n .nav-btn::after {\n content:none !important;\n }\n \n .setting-item {\n display: flex;\n align-items: center;\n justify-content: space-between;\n margin-bottom: 10px;\n padding: 10px;\n border: 1px solid #ddd;\n border-radius: 5px;\n background-color: #f9f9f9;\n }\n .setting-label {\n min-width: 250px;\n font-weight: bold;\n margin-right: 10px;\n }\n .form-content{\n max-width: 150px;\n min-width: 150px;\n }\n .form-content * {\n width: 100%;\n padding: 5px;\n margin-right: 10px;\n min-width: 150px;\n text-align: center;\n }\n .keyword-label {\n display: inline-flex;\n align-items: center;\n padding: 4px 8px;\n border-radius: 4px;\n color: white;\n font-size: 14px;\n position: relative;\n margin-left: 8px;\n margin-bottom: 2px;\n }\n \n .keyword-remove {\n margin-left: 6px;\n cursor: pointer;\n font-size: 12px;\n line-height: 1;\n }\n \n .keyword-input {\n padding: 6px 12px;\n border: 1px solid #ccc;\n border-radius: 4px;\n font-size: 14px;\n float:right;\n }\n \n .add-tag-btn {\n padding: 6px 12px;\n background-color: #45d0b6;\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 14px;\n margin-left: 8px;\n float:right;\n }\n \n .add-tag-btn:hover {\n background-color: #3fceb7;\n }\n #saveBtn {\n padding: 8px 20px;\n background-color: #4CAF50;\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n font-size: 16px;\n margin-top: 10px;\n }\n #saveBtn:hover {\n background-color: #45a049;\n }\n </style\n `; } handle() { isJavDb && $(".navbar-end").prepend('<div class="navbar-item has-dropdown is-hoverable">\n <a id="setting-btn" class="navbar-link nav-btn" style="color: #ff8400 !important;padding-right:15px !important;">\n 设置\n </a>\n </div>'); isJavBus ? $("#navbar").append('\n <ul class="nav navbar-nav navbar-right">\n <li><a id="setting-btn" style="color: #ff8400 !important;padding-right:15px !important;" role="button">设置</a></li>\n </ul>\n ') : $("#pt").append('<div class="z" style="float: right">\n <a id="setting-btn" class="a-primary" style="padding: 0px 16px;">\n 设置\n </a>\n </div>'); $("#setting-btn").on("click", (() => { layer.open({ type: 1, title: "设置", content: '\n <div style=" display: flex; flex-direction: column; height: 100%; ">\n <div style="margin: 20px">\n <a id="importBtn" class="menu-btn" style="background-color:#d25a88"><span>导入数据</span></a>\n <a id="exportBtn" class="menu-btn" style="background-color:#85d0a3"><span>导出数据</span></a>\n <a id="syncDataBtn" class="menu-btn" style="background-color:#387ca9"><span>同步数据</span></a>\n <a id="getRefreshTokenBtn" class="menu-btn fr-btn" style="background-color:#c4a35e"><span>获取refresh_token</span></a>\n\n </div>\n <div style=" flex: 1; overflow-y: auto; margin: 0 20px; padding-bottom: 20px; ">\n <div class="setting-item">\n <span class="setting-label">阿里云盘备份</span>\n <div>\n <a id="backupBtn" class="menu-btn" style="background-color:#5d87c2"><span>备份数据</span></a>\n <a id="backupListBtn" class="menu-btn" style="background-color:#48c554"><span>查看备份</span></a>\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">refresh_token:</span>\n <div class="form-content">\n <input id="refresh_token" >\n </div>\n </div>\n \n <hr style="border: 0; height: 2px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.75), rgba(0,0,0,0));"/>\n \n <div class="setting-item">\n <span class="setting-label">WebDav备份</span>\n <div>\n <a id="webdavBackupBtn" class="menu-btn" style="background-color:#5d87c2"><span>备份数据</span></a>\n <a id="webdavBackupListBtn" class="menu-btn" style="background-color:#48c554"><span>查看备份</span></a>\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">服务地址:</span>\n <div class="form-content">\n <input id="webDavUrl" >\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">用户名:</span>\n <div class="form-content">\n <input id="webDavUsername" >\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">密码:</span>\n <div class="form-content">\n <input id="webDavPassword" >\n </div>\n </div>\n \n <hr style="border: 0; height: 2px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.75), rgba(0,0,0,0));"/>\n \n <div class="setting-item">\n <span class="setting-label">隐藏已屏蔽内容:</span>\n <div class="form-content">\n <select id="hideFilterItem">\n <option value="yes">是</option>\n <option value="no">否</option>\n </select>\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">评论区条数:</span>\n <div class="form-content">\n <select id="reviewCount">\n <option value="10">10条</option>\n <option value="20">20条</option>\n <option value="30">30条</option>\n <option value="40">40条</option>\n <option value="50">50条</option>\n </select>\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">页面列数:</span>\n <div class="form-content" style="position: relative;padding-top: 20px">\n <input type="range" id="containerColumns" min="3" max="10" step="1" style="padding:5px 0">\n <span id="showContainerColumns" style="position:absolute; top:-10px;"></span>\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">页面宽度:</span>\n <div class="form-content" style="position: relative;padding-top: 20px">\n <input type="range" id="containerWidth" min="0" max="30" step="1" style="padding:5px 0">\n <span id="showContainerWidth" style="position:absolute; top:-10px;"></span>\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">每次打开待鉴定待下载数量:</span>\n <div class="form-content">\n <input type="number" id="waitCheckCount" min="1" max="20" style="width: 100%;">\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">详情页打开方式:</span>\n <div class="form-content">\n <select id="dialogOpenDetail">\n <option value="yes">弹窗</option>\n <option value="no">新窗口</option>\n </select>\n </div>\n </div> \n \n <div class="setting-item">\n <span class="setting-label">JavDb地址(用于跟Bus同步数据):</span>\n <div class="form-content">\n <input id="javDbUrl" >\n </div>\n </div>\n \n <hr style="border: 0; height: 2px; margin:20px 0;background-image: linear-gradient(to right, rgba(0,0,0,0), rgba(0,0,0,0.75), rgba(0,0,0,0));"/>\n \n <div class="setting-item">\n <span class="setting-label">评论区屏蔽词:</span>\n <div id="reviewKeywordContainer" style="max-width: 50%;min-width: 50%;">\n <div class="tag-box">\n </div>\n <div style="margin-top: 10px;">\n <button class="add-tag-btn">添加</button>\n <input type="text" class="keyword-input" placeholder="添加屏蔽词">\n </div>\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">视频列表屏蔽词:</span>\n <div id="filterKeywordContainer" style="max-width: 50%;min-width: 50%;">\n <div class="tag-box">\n </div>\n <div style="margin-top: 10px;">\n <button class="add-tag-btn">添加</button>\n <input type="text" class="keyword-input" placeholder="添加屏蔽词">\n </div>\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">屏蔽男演员:</span>\n <div id="filterActorContainer" style="max-width: 50%;min-width: 50%;">\n <div class="tag-box">\n </div>\n <div style="margin-top: 10px;">\n <button class="add-tag-btn">添加</button>\n <input type="text" class="keyword-input" placeholder="添加屏蔽词">\n </div>\n </div>\n </div>\n </div>\n <div style=" flex-shrink: 0; padding: 15px 20px; text-align: right; border-top: 1px solid #eee; background: white; "> \n <button id="saveBtn">保存设置</button>\n </div>\n </div>\n ', area: [ "50%", "80%" ], scrollbar: !1, success: (layero, index) => { $(layero).find(".layui-layer-content").css("position", "relative"); this.loadForm(); this.bindClick(); } }); })); } async loadForm() { let settingObj = await storageManager.getSetting(); $("#hideFilterItem").val(settingObj.hideFilterItem || "yes"); $("#dialogOpenDetail").val(settingObj.dialogOpenDetail || "yes"); $("#reviewCount").val(settingObj.reviewCount || 20); $("#waitCheckCount").val(settingObj.waitCheckCount || 5); $("#containerWidth").val((settingObj.containerWidth || 100) - 70); $("#showContainerWidth").text(settingObj.containerWidth + "%"); $("#containerColumns").val(settingObj.containerColumns || 4); $("#showContainerColumns").text(settingObj.containerColumns || 4); $("#refresh_token").val(settingObj.refresh_token || ""); $("#javDbUrl").val(settingObj.javDbUrl || "https://javdb.com"); $("#webDavUrl").val(settingObj.webDavUrl || ""); $("#webDavUsername").val(settingObj.webDavUsername || ""); $("#webDavPassword").val(settingObj.webDavPassword || ""); let reviewKeywordList = await storageManager.getReviewFilterKeywordList(), filterKeywordList = await storageManager.getTitleFilterKeyword(), filterActorList = await storageManager.getFilterActorList(); reviewKeywordList && reviewKeywordList.forEach((reviewKeyword => { this.addLabelTag("#reviewKeywordContainer", reviewKeyword); })); filterKeywordList && filterKeywordList.forEach((reviewKeyword => { this.addLabelTag("#filterKeywordContainer", reviewKeyword); })); filterActorList && filterActorList.forEach((filterActor => { this.addLabelTag("#filterActorContainer", filterActor); })); [ "#reviewKeywordContainer", "#filterKeywordContainer", "#filterActorContainer" ].forEach((containerId => { $(`${containerId} .add-tag-btn`).on("click", (event2 => this.addKeyword(event2, containerId))); $(`${containerId} .keyword-input`).on("keypress", (event2 => { "Enter" === event2.key && this.addKeyword(event2, containerId); })); })); } bindClick() { $("#importBtn").on("click", (event2 => this.importData(event2))); $("#exportBtn").on("click", (event2 => this.exportData(event2))); $("#syncDataBtn").on("click", (event2 => this.syncData(event2))); $("#backupBtn").on("click", (event2 => this.backupData(event2))); $("#backupListBtn").on("click", (event2 => this.backupListBtn(event2))); $("#webdavBackupBtn").on("click", (event2 => this.backupDataByWebDav(event2))); $("#webdavBackupListBtn").on("click", (event2 => this.backupListBtnByWebDav(event2))); $("#getRefreshTokenBtn").on("click", (event2 => layer.alert("即将跳转阿里云盘, 请登录后, 点击最右侧悬浮按钮获取refresh_token", { yes: function(index, layero, that) { window.open("https://www.aliyundrive.com/drive/home"); layer.close(index); } }))); $("#containerWidth").on("input", (event2 => { const value = parseInt($(event2.target).val()) + 70 + "%"; $("#showContainerWidth").text(value); if (isJavDb) { document.querySelector("section .container").style.minWidth = value; } if (isJavBus) { document.querySelector(".container-fluid .row").style.minWidth = value; } })); $("#containerColumns").on("input", (event2 => { let columns = $("#containerColumns").val(); $("#showContainerColumns").text(columns); if (isJavDb) { document.querySelector(".movie-list").style.gridTemplateColumns = `repeat(${columns}, minmax(0, 1fr))`; } if (isJavBus) { document.querySelector(".masonry").style.gridTemplateColumns = `repeat(${columns}, minmax(0, 1fr))`; } })); $("#saveBtn").on("click", (() => this.saveForm())); } async saveForm() { const hideFilterValue = $("#hideFilterItem").val(), reviewCount = $("#reviewCount").val(), waitCheckCount = $("#waitCheckCount").val(), containerWidth = parseInt($("#containerWidth").val()), containerColumns = parseInt($("#containerColumns").val()), refresh_token = $("#refresh_token").val(); let settingObj = await storageManager.getSetting(); settingObj.hideFilterItem = hideFilterValue; settingObj.reviewCount = reviewCount; settingObj.waitCheckCount = waitCheckCount; settingObj.containerWidth = containerWidth + 70; settingObj.containerColumns = containerColumns; settingObj.refresh_token = refresh_token; settingObj.webDavUrl = $("#webDavUrl").val(); settingObj.webDavUsername = $("#webDavUsername").val(); settingObj.webDavPassword = $("#webDavPassword").val(); settingObj.dialogOpenDetail = $("#dialogOpenDetail").val(); settingObj.javDbUrl = new URL($("#javDbUrl").val()).origin; await storageManager.saveSetting(settingObj); let reviewKeywordList = []; $("#reviewKeywordContainer .keyword-label").toArray().forEach((item => { let keyword = $(item).text().replace("×", "").replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ").trim(); reviewKeywordList.push(keyword); })); await storageManager.saveReviewFilterKeyword(reviewKeywordList); let filterKeywordList = []; $("#filterKeywordContainer .keyword-label").toArray().forEach((item => { let keyword = $(item).text().replace("×", "").replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ").trim(); filterKeywordList.push(keyword); })); await storageManager.saveTitleFilterKeyword(filterKeywordList); let filterActorList = []; $("#filterActorContainer .keyword-label").toArray().forEach((item => { let keyword = $(item).text().replace("×", "").replace(/[\r\n]+/g, " ").replace(/\s{2,}/g, " ").trim(); filterActorList.push(keyword); })); await storageManager.saveFilterActor(filterActorList); show.ok("保存成功"); window.refresh(); } addLabelTag(containerId, keyword) { const $tagBox = $(`${containerId} .tag-box`), $label = $(`\n <div class="keyword-label" style="background-color: #c5b9a0">\n ${keyword}\n <span class="keyword-remove">×</span>\n </div>\n `); $label.find(".keyword-remove").click((event2 => { $(event2.target).parent().remove(); })); $tagBox.append($label); } addKeyword(event2, containerId) { let $keywordInput = $(`${containerId} .keyword-input`); const keyword = $keywordInput.val().trim(); if (keyword) { this.addLabelTag(containerId, keyword); $keywordInput.val(""); } } importData() { try { const input = document.createElement("input"); input.type = "file"; input.accept = ".json"; input.onchange = e => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader; reader.onload = event2 => { try { const content = event2.target.result.toString(), updateJsonData = JSON.parse(content); layer.confirm("确定是否要覆盖导入?", { icon: 3, title: "确认覆盖", btn: [ "确定", "取消" ] }, (async function(index) { await storageManager.importData(updateJsonData); show.ok("数据导入成功"); layer.close(index); location.reload(); })); } catch (err) { console.error(err); show.error("导入失败:文件内容不是有效的JSON格式 " + err); } }; reader.onerror = () => { show.error("读取文件时出错"); }; reader.readAsText(file); }; document.body.appendChild(input); input.click(); setTimeout((() => document.body.removeChild(input)), 1e3); } catch (err) { console.error(err); show.error("导入数据时出错: " + err.message); } } async backupData(event2) { const refresh_token = await storageManager.getSetting("refresh_token"); if (!refresh_token) { show.error("请填写refresh_token并保存后, 再试此功能"); return; } let fileName = utils.getNowStr("_", "_") + ".json", uploadContent = JSON.stringify(await storageManager.exportData()); uploadContent = simpleEncrypt(uploadContent); let loadObj = loading(); try { const aliyunApi = new AliyunApi(refresh_token); await aliyunApi.backup(this.folderName, fileName, uploadContent); show.ok("备份完成"); } catch (e) { console.error(e); show.error(e.toString()); } finally { loadObj.close(); } } async backupListBtn(event2) { const refresh_token = await storageManager.getSetting("refresh_token"); if (!refresh_token) { show.error("请填写refresh_token并保存后, 再试此功能"); return; } let loadObj = loading(); try { const aliyunApi = new AliyunApi(refresh_token), fileList = await aliyunApi.getBackupList(this.folderName); this.openFileListDialog(fileList, aliyunApi, "阿里云盘"); } catch (e) { console.error(e); show.error(`发生错误: ${e ? e.message : e}`); } finally { loadObj.close(); } } async backupDataByWebDav(event2) { const settingObj = await storageManager.getSetting(), webDavUrl = settingObj.webDavUrl; if (!webDavUrl) { show.error("请填写webDav服务地址并保存后, 再试此功能"); return; } const webDavUsername = settingObj.webDavUsername; if (!webDavUsername) { show.error("请填写webDav用户名并保存后, 再试此功能"); return; } const webDavPassword = settingObj.webDavPassword; if (!webDavPassword) { show.error("请填写webDav密码并保存后, 再试此功能"); return; } let fileName = utils.getNowStr("_", "_") + ".json", uploadContent = JSON.stringify(await storageManager.exportData()); uploadContent = simpleEncrypt(uploadContent); let loadObj = loading(); try { const webDavApi = new WebDavApi(webDavUrl, webDavUsername, webDavPassword); await webDavApi.backup(this.folderName, fileName, uploadContent); show.ok("备份完成"); } catch (e) { console.error(e); show.error(e.toString()); } finally { loadObj.close(); } } async backupListBtnByWebDav(event2) { const settingObj = await storageManager.getSetting(), webDavUrl = settingObj.webDavUrl; if (!webDavUrl) { show.error("请填写webDav服务地址并保存后, 再试此功能"); return; } const webDavUsername = settingObj.webDavUsername; if (!webDavUsername) { show.error("请填写webDav用户名并保存后, 再试此功能"); return; } const webDavPassword = settingObj.webDavPassword; if (!webDavPassword) { show.error("请填写webDav密码并保存后, 再试此功能"); return; } let loadObj = loading(); try { const webDavApi = new WebDavApi(webDavUrl, webDavUsername, webDavPassword), fileList = await webDavApi.getBackupList(this.folderName); this.openFileListDialog(fileList, webDavApi, "WebDav"); } catch (e) { console.error(e); show.error(`发生错误: ${e ? e.message : e}`); } finally { loadObj.close(); } } openFileListDialog(fileList, api, apiType) { layer.open({ type: 1, title: apiType + "备份文件", content: '<div id="table-container"></div>', area: [ "40%", "70%" ], success: layero => { const tableObj = new TableGenerator({ containerId: "table-container", columns: [ { key: "name", title: "文件名" }, { key: "createTime", title: "备份日期", render: item => `${utils.getNowStr("-", ":", item.createTime)}` }, { key: "size", title: "文件大小", render: item => { const units = [ "B", "KB", "MB", "GB", "TB", "PB" ]; let unitIndex = 0, adjustedSize = item.size; for (;adjustedSize >= 1024 && unitIndex < units.length - 1; ) { adjustedSize /= 1024; unitIndex++; } return `${adjustedSize % 1 == 0 ? adjustedSize.toFixed(0) : adjustedSize.toFixed(2)} ${units[unitIndex]}`; } } ], data: fileList, buttons: [ { text: "删除", class: "a-danger", onClick: async (event2, item) => { layer.confirm(`是否删除 ${item.name} ?`, { icon: 3, title: "提示", btn: [ "确定", "取消" ] }, (async index => { layer.close(index); let loadObj = loading(); try { await api.deleteFile(item.fileId); let newFileList = await api.getBackupList(this.folderName); tableObj.update(newFileList); "阿里云盘" === apiType ? layer.alert("已移至回收站, 请到阿里云盘回收站二次删除") : layer.alert("删除成功"); } catch (e) { console.error(e); show.error(`发生错误: ${e ? e.message : e}`); } finally { loadObj.close(); } })); } }, { text: "下载", class: "a-primary", onClick: item => { let loadObj = loading(); try { "阿里云盘" === apiType ? api.getDownloadUrl(item.fileId).then((url => { gmHttp.get(url, null, { Referer: "https://www.aliyundrive.com/" }).then((content => { content = simpleDecrypt(content); utils.download(content, item.name); })); })).catch((e => { console.error(e); show.error("下载失败: " + e); })) : api.getFileContent(item.fileId).then((content => { content = simpleDecrypt(content); utils.download(content, item.name); })); } catch (e) { console.error(e); show.error("下载失败: " + e); } finally { loadObj.close(); } } }, { text: "导入", class: "a-success", onClick: item => { layer.confirm(`是否将该云备份数据 ${item.name} 导入?`, { icon: 3, title: "提示", btn: [ "确定", "取消" ] }, (async index => { layer.close(index); let loadObj = loading(); try { let content; if ("阿里云盘" === apiType) { const downUrl = await api.getDownloadUrl(item.fileId); content = await gmHttp.get(downUrl, null, { Referer: "https://www.aliyundrive.com/" }); } else content = await api.getFileContent(item.fileId); content = simpleDecrypt(content); const updateJsonData = JSON.parse(content); await storageManager.importData(updateJsonData); show.ok("导入成功!"); window.location.reload(); } catch (err) { console.error(err); show.error(err); } finally { loadObj.close(); } })); } } ] }); } }); } async exportData(event2) { try { const backupData = JSON.stringify(await storageManager.exportData()), fileName = `${utils.getNowStr("_", "_")}.json`; utils.download(backupData, fileName); show.ok("数据导出成功"); } catch (err) { console.error(err); show.error("导出数据时出错: " + err.message); } } async syncData(event2) { let msg = null, targetUrl = null; if (isJavDb) { msg = "是否将JavBus的数据及配置同步到本站中? "; targetUrl = "https://www.javbus.com/temp?syncData=1"; } if (isJavBus) { msg = "是否将JavDB的数据及配置同步到本站中? "; targetUrl = await storageManager.getSetting("javDbUrl", "https://javdb.com") + "/feedbacks/new?syncData=1"; } utils.q(event2, msg, (() => { const targetWindow = window.open(targetUrl); let targetOrigin = new URL(targetUrl).origin; console.log("开始连接接受方:", targetOrigin); let pingInterval, retryCount = 0; if (!this.hasListenMsg) { window.addEventListener("message", (event3 => { if (event3.origin === targetOrigin) if ("ok" === event3.data) { clearInterval(pingInterval); console.log("连接确认,开始同步数据"); targetWindow.postMessage("syncData", targetOrigin); } else { const resData = event3.data; console.log("收到数据", resData); __privateMethod(this, _SettingPlugin_instances, handleSyncData_fn).call(this, resData); } })); this.hasListenMsg = !0; } const pingTarget = () => { if (retryCount >= 8) { clearInterval(pingInterval); console.log("超过最大重试次数,停止尝试"); show.error("同步失败, 目标网站已中断, 请检查是否登录后再试!", { close: !0, duration: -1 }); } else { console.log(`第 ${retryCount + 1} 次ping...`); targetWindow.postMessage("ping", targetOrigin); retryCount++; } }; pingInterval = setInterval(pingTarget, 1e3); pingTarget(); })); } } _SettingPlugin_instances = new WeakSet; handleSyncData_fn = async function(resData) { try { const targetCarList = resData.carList || [], targetFilterActor = resData.filterActor || [], targetTitleFilterKeyword = resData.titleFilterKeyword || [], targetReviewFilterKeyword = resData.reviewFilterKeyword || [], targetSetting = resData.setting || {}, selfCarList = await storageManager.getCarList() || [], selfFilterActor = await storageManager.getFilterActorList() || [], selfTitleFilterKeyword = await storageManager.getTitleFilterKeyword() || [], selfReviewFilterKeyword = await storageManager.getReviewFilterKeywordList() || [], selfSetting = await storageManager.getSetting() || {}, newCarList = [ ...selfCarList ]; targetCarList.forEach((targetCar => { selfCarList.some((selfCar => selfCar.carNum === targetCar.carNum)) || newCarList.push(targetCar); })); const newFilterActorKeyword = [ ...new Set([ ...selfFilterActor, ...targetFilterActor ]) ], newTitleFilterKeyword = [ ...new Set([ ...selfTitleFilterKeyword, ...targetTitleFilterKeyword ]) ], newReviewFilterKeyword = [ ...new Set([ ...selfReviewFilterKeyword, ...targetReviewFilterKeyword ]) ], newSetting = { ...selfSetting }; Object.keys(targetSetting).forEach((key => { key in newSetting && newSetting[key] || (newSetting[key] = targetSetting[key]); })); await storageManager.overrideCarList(newCarList); await storageManager.saveFilterActor(newFilterActorKeyword); await storageManager.saveTitleFilterKeyword(newTitleFilterKeyword); await storageManager.saveReviewFilterKeyword(newReviewFilterKeyword); await storageManager.saveSetting(newSetting); show.ok("同步完成, 关闭提示后, 将重载数据", { close: !0, duration: -1, callback: () => { window.location.reload(); } }); } catch (error) { console.error(error); show.error("同步数据时出错:", error); } }; const SALT = "x7k9p3"; function simpleEncrypt(str) { return (SALT + str + SALT).split("").map((char => { const code = char.codePointAt(0); return String.fromCodePoint(code + 5); })).join(""); } function simpleDecrypt(encryptedStr) { return encryptedStr.split("").map((char => { const code = char.codePointAt(0); return String.fromCodePoint(code - 5); })).join("").slice(SALT.length, -SALT.length); } class SyncDataPlugin extends BasePlugin { async handle() { if (!window.location.href.includes("syncData=1")) return; isJavBus && $("h4").html("临时页面, 用于同步数据"); let senderOrigin = null; isJavDb && (senderOrigin = "https://www.javbus.com"); isJavBus && (senderOrigin = await storageManager.getSetting("javDbUrl", "https://javdb.com")); console.log("等待发送方:", senderOrigin); window.addEventListener("message", (async event2 => { if (event2.origin === senderOrigin) if ("ping" === event2.data) { console.log("收到 ping,发送确认"); event2.source.postMessage("ok", event2.origin); } else if ("syncData" === event2.data) { console.log("开始发送数据..."); const carList = await storageManager.getCarList(), filterActor = await storageManager.getFilterActorList(), titleFilterKeyword = await storageManager.getTitleFilterKeyword(), reviewFilterKeyword = await storageManager.getReviewFilterKeywordList(), setting = await storageManager.getSetting(); event2.source.postMessage({ carList: carList, filterActor: filterActor, titleFilterKeyword: titleFilterKeyword, reviewFilterKeyword: reviewFilterKeyword, setting: setting }, event2.origin); show.ok("数据已传输, 即将关闭页面...", { callback: () => { window.close(); } }); } })); } } class BusPreviewVideoPlugin extends BasePlugin { async initCss() { return "\n .video-control-btn {\n min-width:100px;\n padding: 8px 16px;\n background: rgba(0,0,0,0.7);\n color: white;\n border: none;\n border-radius: 4px;\n cursor: pointer;\n }\n .video-control-btn.active {\n background-color: #1890ff; /* 选中按钮的背景色 */\n color: white; /* 选中按钮的文字颜色 */\n font-weight: bold; /* 加粗显示 */\n border: 2px solid #096dd9; /* 边框样式 */\n }\n "; } handle() { if (!isDetailPage) return; const firstImageSrc = $("#sample-waterfall a:first").attr("href"), videoPreview = $(`\n <a class="preview-video-container sample-box" style="cursor: pointer">\n <div class="photo-frame" style="position:relative;">\n <img src="${firstImageSrc}" class="video-cover" alt="">\n <div class="play-icon" style="position:absolute; top:50%; left:50%; transform:translate(-50%,-50%); \n color:white; font-size:40px; text-shadow:0 0 10px rgba(0,0,0,0.5);">\n ▶\n </div>\n </div>\n </a>`); $("#sample-waterfall").prepend(videoPreview); let $preview = $(".preview-video-container"); $preview.on("click", (async event2 => { event2.preventDefault(); event2.stopPropagation(); if (!$("#preview-video").length) { let videoUrl = await this.parseVideo(firstImageSrc); console.log("解析播放地址:", videoUrl); $("#magneturlpost").next().after(`<div><video id="preview-video" controls style="width: 100%;margin-top: 5px;"><source src="${videoUrl}" /></video></div>`); this.handleVideo().then((() => { const element = document.getElementById("preview-video"); if (element) { const rect = element.getBoundingClientRect(); window.scrollTo({ top: window.scrollY + rect.top - 100, behavior: "smooth" }); } })); } })); window.location.href.includes("autoPlay=1") && $preview[0].click(); } async handleVideo() { const $videoEl = $("#preview-video"), $previewSource = $videoEl.find("source"), $videoContainer = $videoEl.parent(); if (!$videoEl.length || !$previewSource.length) return; const videoEl = $videoEl[0]; videoEl.muted = !1; videoEl.play(); $videoContainer.css("position", "relative"); const videoSrc = $previewSource.attr("src"), qualityLevels = [ "hhb", "hmb", "mhb", "mmb" ], currentQuality = qualityLevels.find((q => videoSrc.includes(q))) || "mhb", qualityOptions = [ { id: "video-mmb", text: "低画质", quality: "mmb" }, { id: "video-mhb", text: "中画质", quality: "mhb" }, { id: "video-hmb", text: "高画质", quality: "hmb" }, { id: "video-hhb", text: "超高清", quality: "hhb" } ]; const cacheKey = `videoQualities_${this.getPageInfo().carNum}`; let availableQualities = JSON.parse(sessionStorage.getItem(cacheKey)); if (!availableQualities) { availableQualities = (await Promise.all(qualityOptions.map((async option => { const testSrc = videoSrc.replace(new RegExp(qualityLevels.join("|"), "g"), option.quality); try { return (await fetch(testSrc, { method: "HEAD" })).ok ? option : null; } catch { return null; } })))).filter(Boolean); availableQualities.length && sessionStorage.setItem(cacheKey, JSON.stringify(availableQualities)); } if (availableQualities.length <= 1) return; const buttonsHtml = availableQualities.map(((option, index) => `\n <button class="video-control-btn${option.quality === currentQuality ? " active" : ""}" \n id="${option.id}" \n data-quality="${option.quality}"\n style="bottom: ${50 * index}px; right: -105px;">\n ${option.text}\n </button>\n `)).join(""); $videoContainer.append(buttonsHtml); const $buttons = $videoContainer.find(".video-control-btn"); $videoContainer.on("click", ".video-control-btn", (async e => { const $button = $(e.currentTarget), quality = $button.data("quality"); if (!$button.hasClass("active")) try { const newSrc = videoSrc.replace(new RegExp(qualityLevels.join("|"), "g"), quality); $previewSource.attr("src", newSrc); videoEl.load(); videoEl.muted = !1; await videoEl.play(); $buttons.removeClass("active"); $button.addClass("active"); } catch (error) { console.error("切换画质失败:", error); } })); $buttons.last().trigger("click"); } async parseVideo(imgSrc) { const cacheKey = `ok_url_${this.getPageInfo().carNum}`; let ok_url = sessionStorage.getItem(cacheKey); if (ok_url) return ok_url; const videoIdMatch = imgSrc.match(/\/digital\/video\/([^\/]+)\//); if (!videoIdMatch || videoIdMatch.length < 2) { show.error("解析id错误" + imgSrc + ", 该视频没有对应的dmm视频"); console.error("解析dmm视频id错误", imgSrc); setTimeout((() => { $("#preview-video").remove(); }), 1e3); return null; } const videoId = videoIdMatch[1], firstChar = videoId.charAt(0).toLowerCase(); let idPrefix = videoId.substring(0, 3); const checkVideo = async testSrc => { try { console.log("测试视频地址", testSrc); return (await fetch(testSrc, { method: "HEAD" })).ok ? testSrc : null; } catch { return null; } }; let videoIdNo00 = videoId.replace("00", ""), testSrcList = [ `https://cc3001.dmm.co.jp/litevideo/freepv/${firstChar}/${idPrefix}/${videoId}/${videoId}hhb.mp4`, `https://cc3001.dmm.co.jp/litevideo/freepv/${firstChar}/${idPrefix}/${videoIdNo00}/${videoIdNo00}hhb.mp4`, `https://cc3001.dmm.co.jp/litevideo/freepv/${firstChar}/${idPrefix}/${videoId}/${videoId}mhb.mp4`, `https://cc3001.dmm.co.jp/litevideo/freepv/${firstChar}/${idPrefix}/${videoIdNo00}/${videoIdNo00}mhb.mp4` ], okUrl = null; for (let i = 0; i < testSrcList.length; i++) { let testUrl = await checkVideo(testSrcList[i]); if (testUrl) { console.log("测试成功,", testUrl); okUrl = testUrl; break; } } if (!okUrl) { show.error("解析dmm预览视频失败, 请联系作者, 提供番号信息"); throw new Error("解析dmm预览视频失败, 请联系作者, 提供番号信息"); } sessionStorage.setItem(cacheKey, okUrl); return okUrl; } } class SearchByImagePlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "siteList", [ { name: "Google旧版", url: "https://www.google.com/searchbyimage?image_url={占位符}&client=firefox-b-d", ico: "https://www.google.com/favicon.ico" }, { name: "Google", url: "https://lens.google.com/uploadbyurl?url={占位符}", ico: "https://www.google.com/favicon.ico" }, { name: "Yandex", url: "https://yandex.ru/images/search?rpt=imageview&url={占位符}", ico: "https://yandex.ru/favicon.ico" } ]); } async initCss() { return "\n <style>\n #upload-area {\n border: 2px dashed #85af68;\n border-radius: 8px;\n padding: 40px;\n text-align: center;\n margin-bottom: 20px;\n transition: all 0.3s;\n background-color: #f9f9f9;\n }\n #upload-area:hover {\n border-color: #76b947;\n background-color: #f0f0f0;\n }\n /* 拖拽进入 */\n #upload-area.highlight {\n border-color: #2196F3;\n background-color: #e3f2fd;\n }\n \n \n #select-image-btn {\n background-color: #4CAF50;\n color: white;\n border: none;\n padding: 10px 20px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 16px;\n transition: background-color 0.3s;\n }\n #select-image-btn:hover {\n background-color: #45a049;\n }\n \n \n #handle-btn, #cancel-btn {\n padding: 8px 16px;\n border-radius: 4px;\n cursor: pointer;\n font-size: 14px;\n border: none;\n transition: opacity 0.3s;\n }\n #handle-btn {\n background-color: #2196F3;\n color: white;\n }\n #handle-btn:hover {\n opacity: 0.9;\n }\n #cancel-btn {\n background-color: #f44336;\n color: white;\n }\n #cancel-btn:hover {\n opacity: 0.9;\n }\n \n .site-btns-container {\n display: flex;\n flex-wrap: wrap;\n gap: 10px;\n margin-top: 15px;\n }\n .site-btn {\n display: flex;\n align-items: center;\n padding: 8px 12px;\n background-color: #f5f5f5;\n border-radius: 4px;\n text-decoration: none;\n color: #333;\n transition: all 0.2s;\n font-size: 14px;\n border: 1px solid #ddd;\n }\n .site-btn:hover {\n background-color: #e0e0e0;\n transform: translateY(-2px);\n box-shadow: 0 2px 5px rgba(0,0,0,0.1);\n }\n .site-btn img {\n width: 16px;\n height: 16px;\n margin-right: 6px;\n }\n .site-btn span {\n white-space: nowrap;\n }\n </style>\n "; } open() { layer.open({ type: 1, title: "以图识图", content: '\n <div style="padding: 20px">\n <div id="upload-area">\n <div style="color: #555;margin-bottom: 15px;">\n <p>拖拽图片到此处 或 点击按钮选择图片</p>\n <p>也可以直接 Ctrl+V 粘贴图片或 图片URL</p>\n </div>\n <button id="select-image-btn">选择图片</button>\n <input type="file" style="display: none" id="image-file" accept="image/*">\n </div>\n \n <div id="url-input-container" style="margin-top: 15px;display: none;">\n <input type="text" id="image-url" placeholder="粘贴图片URL地址..." style="width: 100%; padding: 8px; border: 1px solid #ddd; border-radius: 4px; box-sizing: border-box;">\n </div>\n \n <div id="preview-area" style="margin-bottom: 20px; text-align: center; display: none;">\n <img id="preview-image" alt="" src="" style="max-width: 100%; max-height: 300px; border-radius: 4px; box-shadow: 0 2px 5px rgba(0,0,0,0.1);">\n <div style="margin-top: 15px; display: flex; justify-content: center; gap: 10px;" id="action-btns">\n <button id="handle-btn">搜索图片</button>\n <button id="cancel-btn">取消</button>\n </div>\n \n <div id="search-results" style="display: none;">\n <p style="margin: 20px auto">请选择识图网站:<a id="openAll" style="cursor: pointer">全部打开</a></p>\n <div class="site-btns-container" id="site-btns-container"></div>\n </div>\n </div>\n \n </div>\n ', area: [ "40%", "80%" ], success: async layero => { this.initEventListeners(); } }); } initEventListeners() { const $uploadArea = $("#upload-area"), $fileInput = $("#image-file"), $selectBtn = $("#select-image-btn"), $previewArea = $("#preview-area"), $previewImage = $("#preview-image"), $actionBtns = $("#action-btns"), $searchImage = $("#handle-btn"), $cancelBtn = $("#cancel-btn"), $urlInputContainer = $("#url-input-container"), $imageUrlInput = $("#image-url"), $searchResults = $("#search-results"), $siteBtnsContainer = $("#site-btns-container"); $uploadArea.on("dragover", (e => { e.preventDefault(); $uploadArea.addClass("highlight"); })).on("dragleave", (() => { $uploadArea.removeClass("highlight"); })).on("drop", (e => { e.preventDefault(); $uploadArea.removeClass("highlight"); if (e.originalEvent.dataTransfer.files && e.originalEvent.dataTransfer.files[0]) { this.handleImageFile(e.originalEvent.dataTransfer.files[0]); this.resetSearchUI(); } })); $selectBtn.on("click", (() => { $fileInput.trigger("click"); })); $fileInput.on("change", (e => { if (e.target.files && e.target.files[0]) { this.handleImageFile(e.target.files[0]); this.resetSearchUI(); } })); $(document).on("paste", (async e => { const items = e.originalEvent.clipboardData.items; for (let i = 0; i < items.length; i++) if (-1 !== items[i].type.indexOf("image")) { const blob = items[i].getAsFile(); this.handleImageFile(blob); this.resetSearchUI(); return; } const text = e.originalEvent.clipboardData.getData("text"); if (text && utils.isUrl(text)) { $urlInputContainer.show(); $imageUrlInput.val(text); $previewImage.attr("src", text); $previewArea.show(); this.resetSearchUI(); } })); $searchImage.on("click", (() => { const imageSrc = $previewImage.attr("src"); imageSrc ? this.searchByImage(imageSrc).then((imgUrl => { $actionBtns.hide(); $searchResults.show(); $siteBtnsContainer.empty(); this.siteList.forEach((site => { const siteUrl = site.url.replace("{占位符}", encodeURIComponent(imgUrl)); $siteBtnsContainer.append(`\n <a href="${siteUrl}" class="site-btn" target="_blank" title="${site.name}">\n <img src="${site.ico}" alt="${site.name}">\n <span>${site.name}</span>\n </a>\n `); })); $siteBtnsContainer.show(); })) : show.info("请粘贴或上传图片"); })); $cancelBtn.on("click", (() => { $previewArea.hide(); $urlInputContainer.hide(); $fileInput.val(""); $imageUrlInput.val(""); })); $imageUrlInput.on("change", (() => { if (utils.isUrl($imageUrlInput.val())) { $previewImage.attr("src", $imageUrlInput.val()); $previewArea.show(); } })); $("#openAll").on("click", (() => { $(".site-btn").toArray().forEach((item => { window.open($(item).attr("href")); })); })); } resetSearchUI() { $("#action-btns").show(); $("#search-results").hide(); $("#site-btns-container").hide().empty(); } handleImageFile(file) { const previewImage = document.getElementById("preview-image"), previewArea = document.getElementById("preview-area"), urlInputContainer = document.getElementById("url-input-container"); if (!file.type.match("image.*")) { show.info("请选择图片文件"); return; } const reader = new FileReader; reader.onload = e => { previewImage.src = e.target.result; previewArea.style.display = "block"; urlInputContainer.style.display = "none"; }; reader.readAsDataURL(file); } async searchByImage(imageSrc) { let loadObj = loading(); try { let imageUrl = imageSrc; if (imageSrc.startsWith("data:")) { show.info("开始上传图片..."); const imgurUrl = await async function(base64Data) { var _a; const matches = base64Data.match(/^data:(.+);base64,(.+)$/); if (!matches || matches.length < 3) throw new Error("无效的Base64图片数据"); const mimeType = matches[1], imageData = matches[2], byteCharacters = atob(imageData), byteNumbers = new Array(byteCharacters.length); for (let i = 0; i < byteCharacters.length; i++) byteNumbers[i] = byteCharacters.charCodeAt(i); const byteArray = new Uint8Array(byteNumbers), blob = new Blob([ byteArray ], { type: mimeType }), formData = new FormData; formData.append("image", blob); const response = await fetch("https://api.imgur.com/3/image", { method: "POST", headers: { Authorization: "Client-ID d70305e7c3ac5c6" }, body: formData }), data = await response.json(); if (data.success && data.data && data.data.link) return data.data.link; throw new Error((null == (_a = data.data) ? void 0 : _a.error) || "上传到Imgur失败"); }(imageSrc); if (!imgurUrl) { show.error("上传到失败"); return; } imageUrl = imgurUrl; } return imageUrl; } catch (error) { show.error(`搜索失败: ${error.message}`); console.error("搜索失败:", error); } finally { loadObj.close(); } } } class BusNavBarPlugin extends BasePlugin { handle() { $("#navbar > div > div > span").append('\n <button class="btn btn-default" style="color: #0d9488" id="search-img-btn">识图</button>\n '); $("#search-img-btn").on("click", (() => { this.getBean("SearchByImagePlugin").open(); })); } } class RelatedPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "floorIndex", 1); } async showRelated() { let movieId = this.getPageInfo().movieId, $magnets = $("#magnets-content"); $magnets.append('\n <div style=" display: flex; align-items: center; margin: 16px 0; color: #666; font-size: 14px; ">\n <span style=" flex: 1; height: 1px; background: linear-gradient(to right, transparent, #999, transparent); "></span>\n <span style="padding: 0 10px;">相关清单</span>\n <a id="relatedFold" style=" margin-left: 8px; color: #1890ff; text-decoration: none; display: flex; align-items: center; ">\n <span class="toggle-text">展开</span>\n <span class="toggle-icon" style="margin-left: 4px;">▼</span>\n </a>\n <span style=" flex: 1; height: 1px; background: linear-gradient(to right, transparent, #999, transparent); "></span>\n </div>\n '); let dataList = null; $("#relatedFold").on("click", (async () => { const textSpan = $("#relatedFold .toggle-text"), iconSpan = $("#relatedFold .toggle-icon"); if ("展开" === textSpan.text()) { textSpan.text("折叠"); iconSpan.text("▲"); $relatedContainer.show(); $relatedFooter.show(); if (dataList) return; try { $relatedContainer.append('<div id="relate-load" style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">正在查询中...</div>'); let pageNum = 1, reviewCount = 20; dataList = await related(movieId, pageNum, reviewCount); $("#relate-load").remove(); if (!dataList) { $relatedContainer.append('<div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">获取清单失败</div>'); return; } 0 === dataList.length && $relatedContainer.append('<div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">无清单</div>'); this.displayRelated(dataList, $relatedContainer); if (dataList.length === reviewCount) { $relatedFooter.html('\n <button id="loadMoreRelated" style="width:100%; background-color: #e1f5fe; border:none; padding:10px; margin-top:10px; cursor:pointer; color:#0277bd; font-weight:bold; border-radius:4px;">\n 加载更多清单\n </button>\n <div id="reviewsEnd" style="display:none; text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部清单</div>\n '); let currentPage = 1; $("#loadMoreRelated").click((async () => { $("#loadMoreRelated").text("加载中...").prop("disabled", !0); currentPage++; const moreData = await related(movieId, currentPage, reviewCount); this.displayRelated(moreData, $relatedContainer); if (moreData.length < reviewCount) { $("#loadMoreRelated").remove(); $("#reviewsEnd").show(); } else $("#loadMoreRelated").text("加载更多清单").prop("disabled", !1); })); } else dataList.length > 0 && $relatedFooter.html('<div style="text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部清单</div>'); } catch (e) { console.error(e); $("#relate-load").remove(); $relatedContainer.append('<div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">获取失败</div>'); } } else { textSpan.text("展开"); iconSpan.text("▼"); $relatedContainer.hide(); $relatedFooter.hide(); } })); $magnets.append('<div id="relatedContainer"></div>'); $magnets.append('<div id="relatedFooter"></div>'); const $relatedContainer = $("#relatedContainer"), $relatedFooter = $("#relatedFooter"); } displayRelated(dataList, $container) { dataList.length && dataList.forEach((item => { let commentHtml = `\n <div class="item columns is-desktop" style="display:block;margin-top:6px;background-color:#ffffff;padding:10px;margin-left: -10px;word-break: break-word;position:relative;">\n <span style="position:absolute;top:5px;right:10px;color:#999;font-size:12px;">#${this.floorIndex++}</span>\n <span style="position:absolute;bottom:5px;right:10px;color:#999;font-size:12px;">创建时间: ${item.createTime}</span>\n <p><a href="/lists/${item.relatedId}" target="_blank" style="color:#2e8abb">${item.name}</a></p>\n <p style="margin-top: 5px;">收藏次数: ${item.collectionCount} 被查看次数: ${item.viewCount}</p>\n </div>\n `; $container.append(commentHtml); })); } } class WantAndWatchedVideosPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "type", null); } async handle() { if (window.location.href.includes("/want_watch_videos")) { $("h3").append('<a class="a-primary" id="wantWatchBtn" style="padding:10px;">导入至 JSH</a>'); $("#wantWatchBtn").on("click", (event2 => { this.type = Status_FAVORITE; this.importWantWatchVideos(event2, "是否将 想看的影片 导入到 JSH-收藏?"); })); } if (window.location.href.includes("/watched_videos")) { $("h3").append('<a class="a-success" id="wantWatchBtn" style="padding:10px;">导入至 JSH</a>'); $("#wantWatchBtn").on("click", (event2 => { this.type = Status_HAS_DOWN; this.importWantWatchVideos(event2, "是否将 看过的影片 导入到 JSH-已下载?"); })); } } importWantWatchVideos(event2, title) { utils.q(null, `${title} <br/> <span style='color: #f40'>执行此功能前请记得备份数据</span>`, (async () => { let loadObj = loading(); try { await this.parseMovieList(); } catch (e) { console.error(e); } finally { loadObj.close(); } })); } async parseMovieList($dom) { let movieList, nextPageLink; if ($dom) { movieList = $dom.find(this.getSelector().itemSelector); nextPageLink = $dom.find(".pagination-next").attr("href"); } else { movieList = $(this.getSelector().itemSelector); nextPageLink = $(".pagination-next").attr("href"); } for (const element of movieList) { const item = $(element), href = item.find("a").attr("href"), carNum = item.find(".video-title strong").text().trim(); if (href && carNum) try { if (await storageManager.getCar(carNum)) { show.info(`${carNum} 已存在, 跳过`); continue; } await storageManager.saveCar(carNum, href, "", this.type); } catch (error) { console.error(`保存失败 [${carNum}]:`, error); } } if (nextPageLink) { show.info("发现下一页,正在解析:", nextPageLink); await new Promise((resolve => setTimeout(resolve, 1e3))); $.ajax({ url: nextPageLink, method: "GET", success: html => { const parser = new DOMParser, next$dom = $(parser.parseFromString(html, "text/html")); this.parseMovieList(next$dom); }, error: function(err) { console.error(err); show.error("加载下一页失败:" + err.message); } }); } else { show.ok("导入结束!"); window.refresh(); } } } class CopyTitleOrDownImgPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "titleSvg", '<svg t="1747553289744" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="7507" width="200" height="200"><path d="M959.8 150.8c0-2.3-1.9-4.2-4.2-4.2H253.3c-2.3 0-4.2 1.9-4.2 4.2v115.9c0 2.3 1.9 4.2 4.2 4.2h702.3c2.3 0 4.2-1.9 4.2-4.2V150.8z" fill="" p-id="7508"></path><path d="M126.4 208.8m-62.2 0a62.2 62.2 0 1 0 124.4 0 62.2 62.2 0 1 0-124.4 0Z" fill="" p-id="7509"></path><path d="M851.5 453.7c0-2.1-1.8-3.9-3.9-3.9H252.9c-2.1 0-3.9 1.7-3.9 3.9v116.6c0 2.1 1.7 3.9 3.9 3.9h594.7c2.1 0 3.9-1.7 3.9-3.9V453.7z" fill="" p-id="7510"></path><path d="M126.4 512m-62.2 0a62.2 62.2 0 1 0 124.4 0 62.2 62.2 0 1 0-124.4 0Z" fill="" p-id="7511"></path><path d="M851.5 756.9c0-2.1-1.8-3.9-3.9-3.9H252.9c-2.1 0-3.9 1.8-3.9 3.9v116.6c0 2.1 1.7 3.9 3.9 3.9h594.7c2.1 0 3.9-1.7 3.9-3.9V756.9z" fill="" p-id="7512"></path><path d="M126.4 815.2m-62.2 0a62.2 62.2 0 1 0 124.4 0 62.2 62.2 0 1 0-124.4 0Z" fill="" p-id="7513"></path></svg>'); __publicField(this, "carNumSvg", '<svg t="1747552574854" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="3539" width="200" height="200"><path d="M920.337035 447.804932c-6.067182-6.067182-10.918677-11.643178-16.985859-17.71036l48.536436-30.334889-42.469254-109.207238-121.340579 12.134365c-6.067182-6.067182-6.067182-12.134365-12.134365-18.201547-12.134365-12.134365-18.201547-24.267706-24.267706-30.334889-24.26873-36.402071-30.334889-42.469254-54.603619-42.469254H339.116511c-18.201547 0-24.267706 6.067182-54.603619 42.469254-6.067182 6.067182-12.134365 18.201547-24.267706 30.334889 0 0-6.067182 6.067182-12.134365 18.201547l-115.27442-12.134365-48.536436 109.207238 51.090608 24.378223c-6.067182 6.067182-30.334889 34.660404-30.334889 34.660405l-15.542998 22.280446-12.282744 17.018605c-6.067182 12.134365-5.064342 10.868535-5.064342 29.070082v224.480635c0 36.402071 18.201547 60.670801 54.603618 60.670801h115.273397c36.402071 0 54.603619-24.267706 54.603619-54.603619v-18.201547h424.693562v18.201547c0 30.334889 18.201547 54.603619 54.603618 54.603619h115.273397c36.402071 0 60.670801-24.267706 60.670801-60.670801V539.300786c0-42.469254 0.685615-46.662763-11.44875-64.863287-4.731768-6.744611-11.94403-16.196891-20.101827-26.632567z m-35.186383-78.381161l-30.334889 18.201547-12.134365-12.134365c-6.067182-8.899694-12.134365-12.134365-12.134365-18.201547l42.469254-6.067183 12.134365 18.201548z m-533.899776-97.072873h339.755054l78.871325 103.140055H272.378527l78.872349-103.140055zM175.305655 357.290429h36.402071c-6.067182 6.067182-6.067182 12.134365-12.134365 18.201547l-18.201547 6.067183-18.201547-12.134365 12.135388-12.134365z m667.375743 394.35765h-54.603619V678.843936H242.043638v72.804143H132.837424V527.167444c0-12.134365-0.041956-20.662599 1.216711-23.556508 1.258667-2.89391 9.955746-16.924461 21.193695-29.173437l35.722596-38.276768h639.576607l21.917172 20.938891c6.067182 6.067182 21.847587 21.366633 25.712615 28.732392 7.621585 9.996678 6.973832 10.999518 13.041014 23.133883v242.682182h-48.536436zM242.043638 533.234627h133.474944v60.670801H242.043638v-60.670801z m412.559197 0h133.474944v60.670801H654.602835v-60.670801z" p-id="3540"></path></svg>'); __publicField(this, "downSvg", '<svg t="1747552626242" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="4551" width="200" height="200"><path d="M641.6 660l-8.64-64 32-4.32a211.2 211.2 0 0 0-26.72-420.32 215.36 215.36 0 0 0-213.12 192 94.56 94.56 0 0 0 0 11.52v41.28h-64V384v-7.04a153.12 153.12 0 0 1 0-19.52A279.84 279.84 0 0 1 636.16 108H640A275.2 275.2 0 0 1 673.28 656z" fill="#333333" p-id="4552"></path><path d="M490.4 446.24l-7.52-39.84a182.4 182.4 0 0 1 107.52-162.88l29.12-13.28L646.08 288l-29.12 13.28a117.92 117.92 0 0 0-70.08 101.28l6.24 30.4zM392.96 652.32h-78.72A202.24 202.24 0 0 1 256 256l30.72-9.12 18.24 61.28-30.72 9.12a138.24 138.24 0 0 0 39.68 270.72h78.72zM479.2 512h64v320h-64z" fill="#333333" p-id="4553"></path><path d="M510.4 908l-156.32-147.68 43.84-46.4 112.48 106.08 112.8-106.08 43.84 46.56-156.64 147.52z" fill="#333333" p-id="4554"></path></svg>'); } async initCss() { return `\n .box .tags {\n justify-content: space-between;\n }\n .tool-box span{\n opacity:.3\n }\n \n .tool-box span:hover{\n opacity:1\n }\n ${isJavBus ? ".tool-box .icon{ height: 2rem; width: 2rem; }" : ""}\n `; } handle() { if (window.isListPage) { this.addCopy(); this.bindClick(); } } addCopy() { $(this.getSelector().itemSelector).toArray().forEach((ele => { let $box = $(ele); isJavDb && $box.find(".tags").append(`\n <div class="tool-box">\n <span class="titleSvg" title="复制标题" style="margin-right: 15px; color:#c5a45d;">${this.titleSvg}</span>\n <span class="carNumSvg" title="复制番号" style="margin-right: 15px; color:#9f2727;">${this.carNumSvg}</span>\n <span class="downSvg" title="下载封面" style="margin-right: 15px; color:#2ca5c0;">${this.downSvg}</span>\n </div>\n `); isJavBus && $box.find(".photo-info").append(`\n <div class="tool-box">\n <span class="titleSvg" title="复制标题" style="margin-right: 15px; color:#c5a45d;">${this.titleSvg}</span>\n <span class="carNumSvg" title="复制番号" style="margin-right: 15px; color:#9f2727;">${this.carNumSvg}</span>\n <span class="downSvg" title="下载封面" style="margin-right: 15px; color:#2ca5c0;">${this.downSvg}</span>\n </div> \n `); })); } bindClick() { const listPagePlugin = this.getBean("ListPagePlugin"); $(this.getSelector().boxSelector).on("click", ".titleSvg", (event2 => { event2.preventDefault(); event2.stopPropagation(); const $box = $(event2.target).closest(".item"), {carNum: carNum, aHref: aHref, title: title} = listPagePlugin.findCarNumAndHref($box); navigator.clipboard.writeText(title).then((() => { show.info("标题已复制到剪切板, " + title); })).catch((err => { console.error("复制失败: ", err); })); })).on("click", ".carNumSvg", (event2 => { event2.preventDefault(); event2.stopPropagation(); const $box = $(event2.target).closest(".item"), {carNum: carNum, aHref: aHref, title: title} = listPagePlugin.findCarNumAndHref($box); navigator.clipboard.writeText(carNum).then((() => { show.info("番号已复制到剪切板, " + carNum); })).catch((err => { console.error("复制失败: ", err); })); })).on("click", ".downSvg", (event2 => { event2.preventDefault(); event2.stopPropagation(); const $box = $(event2.target).closest(".item"), {carNum: carNum, aHref: aHref, title: title} = listPagePlugin.findCarNumAndHref($box); let $img = $box.find(".cover img"); isJavBus && ($img = $box.find(".photo-frame img")); const url = $img.attr("src"); console.log(url); GM_download({ url: url, name: title + ".jpg" }); })); } } utils.importResource("https://cdn.jsdelivr.net/npm/[email protected]/layer.min.css"); utils.importResource("https://cdn.jsdelivr.net/npm/[email protected]/src/toastify.min.css"); window.onload = async function() { window.isDetailPage = function() { let href = window.location.href; return href.includes("javdb") ? href.includes("/v/") : !!href.includes("javbus") && $("#magnet-table").length > 0; }(); window.isListPage = function() { let href = window.location.href; return href.includes("javdb") ? $(".movie-list").length > 0 : !!href.includes("javbus") && $(".masonry > div .item").length > 0; }(); !function() { const pluginManager = new PluginManager; let hostname = window.location.hostname; if (hostname.includes("javdb")) { pluginManager.register(ListPagePlugin); pluginManager.register(AutoPagePlugin); pluginManager.register(Fc2Plugin); pluginManager.register(FoldCategoryPlugin); pluginManager.register(ListPageButtonPlugin); pluginManager.register(HistoryPlugin); pluginManager.register(SettingPlugin); pluginManager.register(NavBarPlugin); pluginManager.register(HitShowPlugin); pluginManager.register(TOP250Plugin); pluginManager.register(SyncDataPlugin); pluginManager.register(SearchByImagePlugin); pluginManager.register(CopyTitleOrDownImgPlugin); pluginManager.register(DetailPagePlugin); pluginManager.register(ReviewPlugin); pluginManager.register(RelatedPlugin); pluginManager.register(DetailPageButtonPlugin); pluginManager.register(HighlightMagnetPlugin); pluginManager.register(PreviewVideoPlugin); pluginManager.register(FilterTitleKeywordPlugin); pluginManager.register(ActressInfoPlugin); pluginManager.register(OtherSitePlugin); pluginManager.register(WantAndWatchedVideosPlugin); } if (hostname.includes("javbus")) { pluginManager.register(ListPagePlugin); pluginManager.register(ListPageButtonPlugin); pluginManager.register(SettingPlugin); pluginManager.register(HistoryPlugin); pluginManager.register(SyncDataPlugin); pluginManager.register(AutoPagePlugin); pluginManager.register(SearchByImagePlugin); pluginManager.register(BusNavBarPlugin); pluginManager.register(CopyTitleOrDownImgPlugin); pluginManager.register(BusDetailPagePlugin); pluginManager.register(DetailPageButtonPlugin); pluginManager.register(ReviewPlugin); pluginManager.register(FilterTitleKeywordPlugin); pluginManager.register(HighlightMagnetPlugin); pluginManager.register(BusPreviewVideoPlugin); } hostname.includes("javtrailers") && pluginManager.register(JavTrailersPlugin); hostname.includes("subtitlecat") && pluginManager.register(SubTitleCatPlugin); hostname.includes("aliyundrive") && pluginManager.register(AliyunPanPlugin); pluginManager.process().then(); }(); }; }();