您需要先安装一个扩展,例如 篡改猴、Greasemonkey 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 暴力猴,之后才能安装此脚本。
您需要先安装一个扩展,例如 篡改猴 或 Userscripts ,之后才能安装此脚本。
您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。
您需要先安装用户脚本管理器扩展后才能安装此脚本。
Jav-鉴黄师 收藏、屏蔽、标记已下载; 免VIP查看热榜、Top250排行榜、Fc2ppv等数据; 可查看所有评论信息; 支持云盘备份; 以图识图; 字幕搜索;
当前为
// ==UserScript== // @name JAV-JHS // @namespace https://sleazyfork.org/zh-CN/scripts/533695-jav-jhs // @version 1.9.3 // @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://*sehuatang.*/* // @include https://javtrailers.com/* // @include https://subtitlecat.com/* // @include https://www.aliyundrive.com/* // @include https://5masterzzz.site/* // @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 xunlei.com // @connect geilijiasu.com // @connect aliyundrive.com // @connect aliyundrive.net // @connect ja.wikipedia.org // @connect beta.magnet.pics // @connect jdforrepam.com // @connect cc3001.dmm.co.jp // @connect cc3001.dmm.com // @connect www.dmm.co.jp // @connect adult.contents.fc2.com // @connect fc2ppvdb.com // @connect 123av.com // @connect u3c3.com // @connect btsow.pics // @connect 3xplanet.com // @connect memojav.com // @connect * // @grant GM_xmlhttpRequest // @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 Db = { boxSelector: ".movie-list", itemSelector: ".movie-list .item", coverImgSelector: ".cover img", requestDomItemSelector: ".movie-list .item" }, Bus = { boxSelector: ".masonry", itemSelector: ".masonry .item", coverImgSelector: ".photo-frame img", requestDomItemSelector: "#waterfall .item" }, currentHref = window.location.href, isJavDb = currentHref.includes("javdb"), isJavBus = currentHref.includes("javbus"), isSearchPage = currentHref.includes("/search?q") || currentHref.includes("/search/") || currentHref.includes("/users/"), Status_FILTER = "filter", Status_FAVORITE = "favorite", Status_HAS_DOWN = "hasDown", Status_HAS_WATCH = "hasWatch"; 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 position:relative; /* 方便预览视频定位*/\n }\n .masonry .movie-box img {\n max-height: 300px;\n min-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 padding: 0 0;\n }\n \n .navbar-link:not(.is-arrowless) {\n padding-right: 33px;\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-JHS", 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(); this.migrateData().then(); } async migrateData() { var dbName; if (!(await (dbName = "JAV-JSH", new Promise((resolve => { const req = indexedDB.open(dbName); req.onerror = () => resolve(!1); req.onsuccess = () => { req.result.close(); resolve(!0); }; req.onupgradeneeded = e => { e.target.transaction.abort(); resolve(!1); }; }))))) return; const oldStore = localforage.createInstance({ driver: localforage.INDEXEDDB, name: "JAV-JSH", version: 1, storeName: "appData" }), newStore = localforage.createInstance({ driver: localforage.INDEXEDDB, name: "JAV-JHS", version: 1, storeName: "appData" }); oldStore.keys().then((keys => { const promises = keys.map((key => oldStore.getItem(key).then((value => newStore.setItem(key, value))))); return Promise.all(promises); })).then((() => { oldStore.dropInstance({ name: "JAV-JSH" }).then((() => {})); })).catch((err => { console.error(err); })); } 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) { settingObj ? await this.forage.setItem(this.setting_key, settingObj) : show.error("设置对象为空"); } async saveSettingItem(key, value) { if (!key) { show.error("key 不能为空"); return; } let settingObj = await this.getSetting(); settingObj[key] = value; await this.saveSetting(settingObj); } async getReviewFilterKeywordList() { return await this.forage.getItem(this.review_filter_keyword_key) || []; } async saveCar(carNum2, url, actress, actionType) { if (!carNum2) { show.error("番号为空!"); throw new Error("番号为空!"); } if (!url) { show.error("url为空!"); throw new Error("url为空!"); } url.includes("http") || (url = window.location.origin + url); actress && (actress = actress.trim()); const carList = await this.forage.getItem(this.car_list_key) || []; let carData = carList.find((item => item.carNum === carNum2)); if (carData) { carData.url = url; carData.actress = actress; carData.updateDate = utils.getNowStr(); } else { carData = { carNum: carNum2, url: url, actress: actress, status: "", updateDate: utils.getNowStr() }; carList.push(carData); } delete carData.createDate; switch (actionType) { case Status_FILTER: if (carData.status === Status_FILTER) { const msg2 = `${carNum2} 已在屏蔽列表中`; show.error(msg2); throw new Error(msg2); } carData.status = Status_FILTER; break; case Status_FAVORITE: if (carData.status === Status_FAVORITE) { const msg2 = `${carNum2} 已在收藏列表中`; show.error(msg2); throw new Error(msg2); } carData.status = Status_FAVORITE; break; case Status_HAS_DOWN: carData.status = Status_HAS_DOWN; break; case Status_HAS_WATCH: carData.status = Status_HAS_WATCH; 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.updateDate ? new Date(a.updateDate).getTime() : 0; return (b.updateDate ? new Date(b.updateDate).getTime() : 0) - dateA; })); } async getCar(carNum2) { return (await this.getCarList()).find((item => item.carNum === carNum2)); } async removeCar(carNum2) { const carList = await this.getCarList(), initialLength = carList.length, updatedList = carList.filter((car => car.carNum !== carNum2)); if (updatedList.length === initialLength) { show.error(`${carNum2} 不存在`); 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 ? "..." : ""}`); newList.forEach((car => { if (!("updateDate" in car)) if ("createDate" in car) { car.updateDate = car.createDate; delete car.createDate; } else car.updateDate = "2025-04-23 08:14:33"; })); 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, "mimeTypes", { txt: "text/plain", html: "text/html", css: "text/css", csv: "text/csv", json: "application/json", xml: "application/xml", jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif", webp: "image/webp", svg: "image/svg+xml", pdf: "application/pdf", doc: "application/msword", docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation", zip: "application/zip", rar: "application/x-rar-compressed", "7z": "application/x-7z-compressed", mp3: "audio/mpeg", wav: "audio/wav", mp4: "video/mp4", webm: "video/webm", ogg: "audio/ogg" }); __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)) return window.open(url); const finalUrl = url.includes("?") ? `${url}&hideNav=1` : `${url}?hideNav=1`; layer.open({ type: 2, title: title, content: finalUrl, 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) { const elements = parent.document.querySelectorAll(selector); if (elements.length > 0) { const elementToRemove = elements.length > 1 ? elements[elements.length - 1] : elements[0]; elementToRemove.parentNode.removeChild(elementToRemove); } })); 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: [ "确定", "取消" ], shade: 0, 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) { show.info("开始请求下载..."); const fileExtension = fileName.split(".").pop().toLowerCase(); let mimeType = this.mimeTypes[fileExtension] || "application/octet-stream"; const blob = new Blob([ data ], { type: mimeType }), 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; } } setHrefParam(key, val) { const newUrl = new URL(window.location.href); newUrl.searchParams.set(key, val); window.history.pushState({}, "", newUrl.toString()); } getResponsiveArea() { const screenWidth = window.innerWidth; return screenWidth >= 1920 ? [ "60%", "80%" ] : screenWidth >= 1200 ? [ "60%", "85%" ] : screenWidth >= 768 ? [ "70%", "90%" ] : [ "95%", "95%" ]; } isMobile() { const userAgent = navigator.userAgent.toLowerCase(); return [ "iphone", "ipod", "ipad", "android", "blackberry", "windows phone", "nokia", "webos", "opera mini", "mobile", "mobi", "tablet" ].some((keyword => userAgent.includes(keyword))); } } class SeHuaTangStorageManager { constructor() { __publicField(this, "forage", localforage.createInstance({ driver: localforage.INDEXEDDB, name: "JAV-JHS-SeHuaTang", version: 1, storeName: "appData" })); __publicField(this, "article_list_key", "article_list"); if (SeHuaTangStorageManager.instance) throw new Error("SeHuaTangStorageManager已被实例化过了!"); SeHuaTangStorageManager.instance = this; } async saveArticle(articleId, url, title, actionType) { if (!articleId) { show.error("articleId为空!"); throw new Error("articleId为空!"); } if (!title) { show.error("title为空!"); throw new Error("title为空!"); } if (!url) { show.error("url为空!"); throw new Error("url为空!"); } url.includes("http") || (url = window.location.origin + url); const articleList = await this.forage.getItem(this.article_list_key) || []; let articleData = articleList.find((item => item.articleId === articleId)); if (articleData) articleData.updateDate = utils.getNowStr(); else { articleData = { articleId: articleId, url: url, status: "", updateDate: utils.getNowStr() }; articleList.push(articleData); } switch (actionType) { case Status_FILTER: if (articleData.status === Status_FILTER) { const msg2 = `${articleId} 已在屏蔽列表中`; show.error(msg2); throw new Error(msg2); } articleData.status = Status_FILTER; break; case Status_FAVORITE: if (articleData.status === Status_FAVORITE) { const msg2 = `${articleId} 已在收藏列表中`; show.error(msg2); throw new Error(msg2); } articleData.status = Status_FAVORITE; break; default: const msg = "actionType错误"; show.error(msg); throw new Error(msg); } await this.forage.setItem(this.article_list_key, articleList); } async getArticleList() { return await this.forage.getItem(this.article_list_key) || []; } async getArticle(articleId) { return (await this.forage.getItem(this.article_list_key) || []).find((item => item.articleId === articleId)); } } 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, timeout: 1e4, 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); } checkUrlStatus(url, headers = {}, timeout) { return new Promise(((resolve, reject) => { GM_xmlhttpRequest({ method: "HEAD", url: url, headers: headers, timeout: timeout || 1e4, onload: response => { resolve(response.status); }, onerror: error => { reject(new Error(`请求失败: ${error}`)); }, ontimeout: () => { reject(new Error(`请求超时(${timeout}ms)`)); } }); })); } 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, timeout: 1e4, 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 { console.error("请求失败,状态码:", response.status); 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; window.seHuaTangStorageManager = new SeHuaTangStorageManager; 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 carNum2, url, actress, actors, currentHref2 = window.location.href; if (isJavDb) { carNum2 = $('a[title="複製番號"]').attr("data-clipboard-text"); url = currentHref2.split("?")[0].split("#")[0]; actress = $(".female").prev().map(((i, el) => $(el).text())).get().join(" "); actors = $(".male").prev().map(((i, el) => $(el).text())).get().join(" "); } if (isJavBus) { url = currentHref2.split("?")[0]; carNum2 = url.split("/").filter(Boolean).pop(); actress = $('span[onmouseover*="star_"] a').map(((i, el) => $(el).text())).get().join(" "); actors = ""; } return { carNum: carNum2, url: url, actress: actress, actors: actors }; } getSelector() { return isJavDb ? Db : isJavBus ? Bus : null; } parseMovieId(href) { return href.split("/").pop().split(/[?#]/)[0]; } } 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() { if (window.isDetailPage) { this.checkFilterActor().then(); $(".video-meta-panel a").attr("target", "_blank"); } } async checkFilterActor() { if (!window.isDetailPage) return; if (await storageManager.getCar(carNum)) 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); }), (() => { detailPageButtonPlugin.answerCount = 1; })); } })); } } const getDmmVideo = async carNum2 => { const cacheKey = `dmm_video_urls_${carNum2}`, cachedUrls = sessionStorage.getItem(cacheKey); if (cachedUrls) return JSON.parse(cachedUrls); const url = `https://www.dmm.co.jp/search/=/searchstr=${carNum2}`, html = await gmHttp.get(url); let matchVideoHref = $(html).find('a[href*="litevideo/freepv"]:first').attr("href"); if (!matchVideoHref) { show.error("解析dmm失败, 该番号可能没有预览视频, " + url); return null; } let parts = matchVideoHref.split("/"), videoId = parts[parts.length - 2]; const firstChar = videoId.charAt(0); let idPrefix = videoId.substring(0, 3); const videoTypes = [ "hhb", "hmb", "mhb", "mmb" ], videoMap = {}; for (const type of videoTypes) videoMap[type] = `https://cc3001.dmm.co.jp/litevideo/freepv/${firstChar}/${idPrefix}/${videoId}/${videoId}${type}.mp4`; const errorMap = { 403: "节点不可用,请分流到日本ip", 404: "资源不存在", null: "网络错误" }; try { const checkPromises = Object.entries(videoMap).map((([type, url2]) => gmHttp.checkUrlStatus(url2).then((status => ({ type: type, url: url2, status: status }))).catch((e => { console.error(e); return { type: type, url: url2, status: null }; })))), results = await Promise.all(checkPromises), finalMap = {}, errorMessages = new Set; for (const {type: type, url: url2, status: status} of results) if (200 === status) finalMap[type] = url2; else { const msg = errorMap[status] || `未知错误状态码: ${status}`; errorMessages.add(msg); } let finalMapLength = Object.keys(finalMap).length; 0 === finalMapLength && errorMessages.size > 0 && errorMessages.forEach((msg => show.error(msg))); Object.values(finalMap).some((url2 => "" !== url2)) && sessionStorage.setItem(cacheKey, JSON.stringify(finalMap)); return finalMapLength > 0 ? finalMap : null; } catch (error) { console.error("并行检查URL时出错:", error); show.error(errorMap.null); return null; } }; class PreviewVideoPlugin extends BasePlugin { async initCss() { return "\n .video-control-btn {\n position: absolute;\n z-index: 99999999999;\n min-width:120px;\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(); })); })); let href = window.location.href; (href.includes("gallery-1") || href.includes("gallery-2")) && utils.loopDetector((() => $(".fancybox-content #preview-video").length > 0), (() => { $(".fancybox-content #preview-video").length > 0 && this.handleVideo().then(); })); href.includes("autoPlay=1") && $preview[0].click(); } async handleVideo() { const $videoEl = $("#preview-video"), $previewSource = $videoEl.find("source"), $videoContainer = $videoEl.parent(); $videoContainer.css("position", "relative"); if (!$videoEl.length || !$previewSource.length) return; $previewSource.attr("src"); $previewSource.removeAttr("src"); const videoEl = $videoEl[0]; let carNum2 = this.getPageInfo().carNum; const dmmVideoMap = await getDmmVideo(carNum2); if (!dmmVideoMap) return null; let defaultVideoQuality = await storageManager.getSetting("videoQuality") || "hhb"; dmmVideoMap[defaultVideoQuality] || (defaultVideoQuality = Object.keys(dmmVideoMap)[0]); let defaultVideoUrl = dmmVideoMap[defaultVideoQuality]; $previewSource.attr("src", defaultVideoUrl); let buttonsHtml = ""; let activeIndex = 0; [ { id: "video-mmb", quality: "mmb", text: "中画质 (432p)" }, { id: "video-mhb", quality: "mhb", text: "高画质 (576p)" }, { id: "video-hmb", quality: "hmb", text: "HD (720p)" }, { id: "video-hhb", quality: "hhb", text: "FullHD (1080p)" } ].forEach((option => { let dmmVideoUrl = dmmVideoMap[option.quality]; if (dmmVideoUrl) { const isActive = defaultVideoQuality === option.quality; buttonsHtml += `\n <button class="video-control-btn${isActive ? " active" : ""}" \n id="${option.id}" \n data-quality="${option.quality}"\n data-video-src="${dmmVideoUrl}"\n style="bottom: ${50 * activeIndex}px; right: -123px;">\n ${option.text}\n </button>\n `; activeIndex++; } })); let dmmVideoMapLength = Object.keys(dmmVideoMap).length; buttonsHtml = `<button class="menu-btn" id="speed-btn" style="position: absolute; min-width: 120px; background-color:#76b45d;bottom: ${50 * (dmmVideoMapLength + 2)}px; right: -123px;">快进(z)</button>` + buttonsHtml; buttonsHtml = `<button class="menu-btn" id="video-filterBtn" style="position: absolute; min-width: 120px; background-color:#de3333;bottom: ${50 * (dmmVideoMapLength + 1)}px; right: -123px;">屏蔽(a)</button>` + buttonsHtml; buttonsHtml = `<button class="menu-btn" id="video-favoriteBtn" style="position: absolute; min-width: 120px; background-color:#25b1dc;bottom: ${50 * dmmVideoMapLength}px; right: -123px;">收藏(s)</button>` + buttonsHtml; $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); } })); $("#speed-btn").on("click", (() => { this.getBean("DetailPageButtonPlugin").speedVideo(); })); utils.rightClick($("#speed-btn"), (event2 => { this.getBean("DetailPageButtonPlugin").filterOne(event2); })); $("#video-filterBtn").on("click", (event2 => { this.getBean("DetailPageButtonPlugin").filterOne(event2); })); $("#video-favoriteBtn").on("click", (event2 => { this.getBean("DetailPageButtonPlugin").favoriteOne(event2); })); } } 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() { if (!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); })); utils.loopDetector((() => $("#vjs_video_3 canvas").length > 0), (() => { 0 !== $("#vjs_video_3 canvas").length && $("#vjs_video_3 canvas").css({ position: "fixed", width: "100vw", height: "100vh", objectFit: "cover", top: "0", right: "0", zIndex: "999999998" }); })); } } } 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")) { $("section").html(""); const urlParams = new URLSearchParams(window.location.search); let movieId = urlParams.get("movieId"), carNum2 = urlParams.get("carNum"), url = urlParams.get("url"); movieId && carNum2 && url && this.openFc2Dialog(movieId, carNum2, 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 "; } openFc2Dialog(movieId, carNum2, href) { if (href.includes("123av")) { this.getBean("Fc2By123AvPlugin").open123AvFc2Dialog(carNum2, href); return; } layer.open({ type: 1, title: carNum2, 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="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\n \n\x3c!-- <a id="enable-magnets-filter" class="menu-btn fr-btn" style="background-color:#c2bd4c">--\x3e\n\x3c!-- <span id="magnets-span">关闭磁力过滤</span>--\x3e\n\x3c!-- </a>--\x3e\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 <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 <div id="related-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, carNum2); $("#favoriteBtn").on("click", (async event2 => { const actress = $("#data-actress").text(); await storageManager.saveCar(carNum2, href, actress, Status_FAVORITE); window.refresh(); layer.closeAll(); })); $("#filterBtn").on("click", (event2 => { utils.q(event2, `是否屏蔽${carNum2}?`, (async () => { const actress = $("#data-actress").text(); await storageManager.saveCar(carNum2, 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(carNum2, href, actress, Status_HAS_DOWN); window.refresh(); layer.closeAll(); })); $("#hasWatchBtn").on("click", (async event2 => { const actress = $("#data-actress").text(); await storageManager.saveCar(carNum2, href, actress, Status_HAS_WATCH); window.refresh(); layer.closeAll(); })); $("#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=${carNum2}`, carNum2, !1, event2))); $("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2))); }, end() { window.location.href.includes("collection_codes?movieId") && utils.closePage(); } }); } loadData(movieId, carNum2) { this.handleVideo(carNum2.replace("FC2-", "")); this.handleMovieDetail(movieId); this.handleMagnets(movieId); this.getBean("reviewPlugin").showReview(movieId, $("#reviews-content")).then(); this.getBean("RelatedPlugin").showRelated($("#related-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); this.getBean("HighlightMagnetPlugin").handleDb(); })).catch((err => { console.error(err); $("#magnets-content").html(`\n <div class="movie-error">加载失败: ${err.message}</div>\n `); })); } async handleVideo(searchKeyword) { const fc2By123AvPlugin = this.getBean("Fc2By123AvPlugin"); let loadObj = loading(); try { const urls = [ `${fc2By123AvPlugin.baseUrl}/dm4/v/fc2-ppv-${searchKeyword}`, `${fc2By123AvPlugin.baseUrl}/v/fc2-ppv-${searchKeyword}`, `${fc2By123AvPlugin.baseUrl}/dm1/v/fc2-ppv-${searchKeyword}` ]; let success = !1; for (const url of urls) try { const {id: id, moviePoster: moviePoster} = await fc2By123AvPlugin.get123AvVideoInfo(url), pageUrl = await fc2By123AvPlugin.getMovie(id, moviePoster); if (pageUrl) { $(".movie-trailer").attr("src", pageUrl); success = !0; break; } } catch (e) { console.log("尝试失败:", url, e.message); } if (!success) throw new Error("所有地址尝试失败"); } catch (e) { console.error(e); $(".movie-poster-container").html(`\n <div class="movie-not-found">\n <i class="icon-warning"></i>\n <h3>未找到相关视频信息</h3>\n <p>123Av 中没有找到与当前番号相关的影片信息</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 `); $(".movie-trailer").hide(); } finally { loadObj.close(); } } async openFc2Page(movieId, carNum2, url) { let javDbUrl = await storageManager.getSetting("javDbUrl", "https://javdb.com"); window.open(`${javDbUrl}/users/collection_codes?movieId=${movieId}&carNum=${carNum2}&url=${url}`); } } 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('a[title="誕生日"]').parent().parent().find("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(); this.hookOldSearch(); this.toggleOtherNavItem(); $(window).resize(this.toggleOtherNavItem); if (window.location.href.includes("/search?q")) { const urlParams = new URLSearchParams(window.location.search); let q = urlParams.get("q"), f = urlParams.get("f"); $("#search-keyword").val(q); $("#search-type").val(f); } } hookSearch() { $("#navbar-menu-hero").after('\n <div class="navbar-menu" id="search-box">\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(); })); } hookOldSearch() { const searchImage = document.querySelector(".search-image"); if (!searchImage) return; const clonedImage = searchImage.cloneNode(!0); searchImage.parentNode.replaceChild(clonedImage, searchImage); $("#button-search-image").attr("data-tooltip", "以图识图"); $(".search-image").on("click", (event2 => { 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 '); } toggleOtherNavItem() { let $searchBox = $("#search-box"), $oldSearchBox = $("#search-bar-container"); if ($(window).width() < 1600 && $(window).width() > 1023) { $searchBox.hide(); $oldSearchBox.show(); } if ($(window).width() > 1600) { $searchBox.show(); $oldSearchBox.hide(); } } } class OtherSitePlugin extends BasePlugin { handle() { let carNum2 = 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/${carNum2}/" 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/${carNum2}" 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=${carNum2}" 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/${carNum2.toLowerCase().replace("-", "00")}?handle=1`, carNum2, !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(); this.hideVideoControls(); window.isDetailPage && this.createMenuBtn(); } createMenuBtn() { const pageInfo = this.getPageInfo(), carNum2 = pageInfo.carNum, buttonsHtml = '\n <div style="margin: 10px auto;">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333; color:white">\n <span>🚫 屏蔽(a)</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc; color:white">\n <span>⭐ 收藏(s)</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b; color:white">\n <span>📥️ 已下载</span>\n </a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c; color:white">\n <span>🔍 已观看</span>\n </a>\n\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 <a id="magnetSearchBtn" class="menu-btn fr-btn" style="background:linear-gradient(to right, rgb(245,140,1), rgb(84,161,29))">\n <span>磁力搜索</span>\n </a>\n <a id="enable-magnets-filter" class="menu-btn fr-btn" style="background-color:#c2bd4c">\n <span id="magnets-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: 50, callback: () => { utils.closePage(); } }); })); $("#hasWatchBtn").on("click", (async () => { await storageManager.saveCar(pageInfo.carNum, pageInfo.url, pageInfo.actress, Status_HAS_WATCH); window.refresh(); show.ok("操作成功", { duration: 50, callback: () => { utils.closePage(); } }); })); $("#magnetSearchBtn").on("click", (() => { let magnetHub = this.getBean("MagnetHubPlugin").createMagnetHub(pageInfo.carNum); layer.open({ type: 1, title: "磁力搜索", content: '<div id="magnetHubBox"></div>', area: utils.getResponsiveArea(), scrollbar: !1, success: () => { $("#magnetHubBox").append(magnetHub); } }); })); $("#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=${carNum2}`, carNum2, !1, event2))); $("#xunLeiSubtitleBtn").on("click", (() => this.searchXunLeiSubtitle(carNum2))); this.showStatus(carNum2).then(); } async showStatus(carNum2) { let car = await storageManager.getCar(carNum2); 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("📥️ 已标记下载"); break; case Status_HAS_WATCH: $("#hasWatchBtn").text("🔍 已标记观看"); } } async favoriteOne() { let pageInfo = this.getPageInfo(); await storageManager.saveCar(pageInfo.carNum, pageInfo.url, pageInfo.actress, Status_FAVORITE); window.refresh(); show.ok("操作成功", { duration: 50, callback: () => { utils.closePage(); } }); } searchXunLeiSubtitle(carNum2) { let loadObj = loading(); gmHttp.get(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?gcid=&cid=&name=${carNum2}`).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 => { let url = item.url, name = carNum2 + "." + item.ext; this.previewSubtitle(url, name); } }, { text: "下载", class: "a-success", onClick: async item => { let url = item.url, name = carNum2 + "." + item.ext, content = await gmHttp.get(url); utils.download(content, name); } } ] }); } }) : show.error("迅雷中找不到相关字幕!"); })).catch((e => { console.error(e); show.error(e); })).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: 50, 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: 50, callback: () => { utils.closePage(); } }); }), (() => { this.answerCount = 1; })); } speedVideo() { if ($("#preview-video").is(":visible")) { const videoEl = document.getElementById("preview-video"); if (videoEl) { videoEl.muted = !1; videoEl.controls = !1; if (videoEl.currentTime + 5 < videoEl.duration) videoEl.currentTime += 5; else { show.info("预览视频结束, 已回到开头"); videoEl.currentTime = 1; } } 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(); } hideVideoControls() { $(document).on("mouseenter", "#preview-video", (function() { $(this).prop("controls", !0); })); } 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, (event2 => { const activeElement = document.activeElement; "INPUT" === activeElement.tagName || "TEXTAREA" === activeElement.tagName || activeElement.isContentEditable || (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); })); } previewSubtitle(url, name) { if (!url) { console.error("未提供文件URL"); return; } const fileExt = url.split(".").pop().toLowerCase(); "ass" === fileExt || "srt" === fileExt ? gmHttp.get(url).then((text => { let content = text, title = "字幕预览"; if ("ass" === fileExt) { title = "ASS字幕预览 - " + name; const eventsSection = text.match(/\[Events][\s\S]*?(?=\[|$)/i); eventsSection && (content = eventsSection[0]); } else "srt" === fileExt && (title = "SRT字幕预览 - " + name); layer.open({ type: 1, title: title, area: [ "80%", "80%" ], scrollbar: !1, content: `<div style="padding:15px;background:#1E1E1E;color:#FFF;font-family:Consolas,Monaco,monospace;white-space:pre-wrap;overflow:auto;height:100%;">${content}</div>`, btn: [ "下载", "关闭" ], btn1: function(index, layero, that) { utils.download(text, name); return !1; } }); })).catch((error => { show.error(`预览失败: ${error.message}`); console.error("预览字幕文件出错:", error); })) : alert("仅支持预览ASS和SRT字幕文件"); } } class HistoryPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "dataType", "all"); __publicField(this, "tableObj", null); } handle() { if (isJavDb) { let handleResize = function() { if ($(".navbar-search").is(":hidden")) { $(".historyBtnBox").show(); $(".miniHistoryBtnBox").hide(); } else { $(".historyBtnBox").hide(); $(".miniHistoryBtnBox").show(); } }; $(".navbar-end").prepend('<div class="navbar-item has-dropdown is-hoverable historyBtnBox">\n <a id="historyBtn" class="navbar-link nav-btn" style="color: #aade66 !important;padding-right:15px !important;">\n 历史列表\n </a>\n </div>'); $(".navbar-search").css("margin-left", "0").before('\n <div class="navbar-item miniHistoryBtnBox">\n <a id="miniHistoryBtn" class="navbar-link nav-btn" style="color: #aade66 !important;padding-left:0 !important;padding-right:0 !important;">\n 历史列表\n </a>\n </div>\n '); handleResize(); $(window).resize(handleResize); } 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,#miniHistoryBtn").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:#de3333">🚫 已屏蔽</a>\n <a class="menu-btn history-btn" data-action="favorite" style="background-color:#25b1dc;">⭐ 已收藏</a>\n <a class="menu-btn history-btn" data-action="hasDown" style="background-color:#7bc73b;">📥️ 已下载</a>\n <a class="menu-btn history-btn" data-action="hasWatch" style="background-color:#d7a80c;">🔍 已观看</a>\n </div>\n <div id="table-container"></div>\n ', area: utils.getResponsiveArea(), 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 movieId = this.parseMovieId(data.url); this.getBean("fc2Plugin").openFc2Dialog(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 movieId = this.parseMovieId(url); await this.getBean("Fc2Plugin").openFc2Page(movieId, data.carNum, url); } 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; this.hasWatchCount = 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++; break; case Status_HAS_WATCH: this.hasWatchCount++; } })); $('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})`); $('a[data-action="hasWatch"]').text(`🔍 已观看 (${this.hasWatchCount})`); 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: "updateDate", title: "操作日期", width: "185px", render: item => "updateDate" in item ? item.updateDate : item.createDate || "" }, { 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>' : url.includes("123av") ? '<span style="color:#eaa813">123Av</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 = "📥️ 已下载"; break; case "hasWatch": 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 movieId = this.parseMovieId(window.location.href); await this.showReview(movieId); await this.getBean("RelatedPlugin").showRelated(); } if (isJavBus) { let carNum2 = 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; })(carNum2); let movieId = null; for (let i = 0; i < movies.length; i++) { let item = movies[i]; if (item.number.toLowerCase() === carNum2.toLowerCase()) { movieId = item.id; break; } } if (!movieId) { show.error("解析视频ID失败, 该视频可能不存在, 无法获取评论数据"); return; } this.showReview(movieId, $("#sample-waterfall")).then(); } } } async showReview(movieId, $eleBox) { const $magnets = $eleBox || $("#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="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", (event2 => { event2.preventDefault(); event2.stopPropagation(); const $text = $("#reviewsFold .toggle-text"), $icon = $("#reviewsFold .toggle-icon"), isFolded = "展开" === $text.text(); $text.text(isFolded ? "折叠" : "展开"); $icon.text(isFolded ? "▲" : "▼"); if (isFolded) { $("#reviewsContainer").show(); $("#reviewsFooter").show(); } else { $("#reviewsContainer").hide(); $("#reviewsFooter").hide(); } })); $magnets.append('<div id="reviewsContainer"></div>'); $magnets.append('<div id="reviewsFooter"></div>'); await this.fetchAndDisplayReviews(movieId); } async fetchAndDisplayReviews(movieId) { const $reviewsContainer = $("#reviewsContainer"), $reviewsFooter = $("#reviewsFooter"); $reviewsContainer.append('<div id="reviewsLoading" style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">获取评论中...</div>'); const reviewCount = await storageManager.getSetting("reviewCount", 20); let dataList = null; try { dataList = await getReviews(movieId, 1, reviewCount); } catch (e) { console.error("获取评论失败:", e); } finally { $("#reviewsLoading").remove(); } if (!dataList) { $reviewsContainer.append('\n <div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">\n 获取评论失败\n <a id="retryFetchReviews" href="javascript:;" style="margin-left: 10px; color: #1890ff; text-decoration: none;">重试</a>\n </div>\n '); $("#retryFetchReviews").on("click", (async () => { $("#retryFetchReviews").parent().remove(); await this.fetchAndDisplayReviews(movieId); })); return; } if (0 === dataList.length) { $reviewsContainer.append('<div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">无评论</div>'); return; } 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 = $("#loadMoreReviews"); $loadMoreReviews.on("click", (async () => { $loadMoreReviews.text("加载中...").prop("disabled", !0); currentPage++; let moreData; try { moreData = await getReviews(movieId, currentPage, reviewCount); } catch (e) { console.error("加载更多评论失败:", e); } finally { $loadMoreReviews.text("加载失败, 请点击重试").prop("disabled", !1); } if (moreData) { this.displayReviews(moreData, $reviewsContainer, reviewKeywordList); if (moreData.length < reviewCount) { $loadMoreReviews.remove(); $("#reviewsEnd").show(); } else $loadMoreReviews.text("加载更多评论").prop("disabled", !1); } })); } else $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 => { if (reviewKeywordList.some((keyword => item.content.includes(keyword)))) return; const starsHtml = Array(item.score).fill('<i class="icon-star"></i>').join(""), content = item.content.replace(/(https?:\/\/[^\s]+|magnet:\?[^\s"'\u4e00-\u9fa5,。?!()【】]+)/gi, (match => `<a href="${match}" class="a-primary" style="padding:0; word-break: break-all; white-space: pre-wrap;" target="_blank" rel="noopener noreferrer">${match}</a>`)), 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">${utils.formatDate(item.created_at)}</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"), (async event2 => { const selectedText = window.getSelection().toString(); if (selectedText) { event2.preventDefault(); if (await utils.q(event2, `是否将 '${selectedText}' 加入评论区关键词?`)) { 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 '); isSearchPage || $(".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 if (window.location.href.includes("advanced_search")) { let $section = $("h2.section-title"); $section.css({ display: "grid", "grid-template-columns": "auto auto 1fr", width: "100%" }); $section.append(`\n <div>\n <a class="menu-btn" id="waitCheckBtn" \n style="background-color:#56c938 !important;; margin-left: 10px;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;border-bottom:none !important; border-radius:3px;">\n <span>打开已收藏</span>\n </a>\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; float: right">\n 当前排序方式: ${"rateCount" === localStorage.getItem("sortMethod") ? "评价人数" : "date" === localStorage.getItem("sortMethod") ? "时间" : "默认"}\n </a>\n </div>\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 '); isSearchPage || $(".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() { if (isSearchPage) return; 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 = this.getSelector(); 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))) return; const {carNum: carNum2, aHref: aHref, title: title} = this.getBean("ListPagePlugin").findCarNumAndHref($el); if (carNum2.includes("FC2-")) { const movieId = this.parseMovieId(aHref); this.getBean("Fc2Plugin").openFc2Page(movieId, carNum2, aHref); } else { let url = aHref + (aHref.includes("?") ? "&autoPlay=1" : "?autoPlay=1"); window.open(url); } 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], carNum2 = data.carNum, url = data.url; if (carNum2.includes("FC2-")) { const movieId = this.parseMovieId(url); await this.getBean("Fc2Plugin").openFc2Page(movieId, carNum2, url); } else window.open(url); } } } class ListPagePlugin extends BasePlugin { async handle() { this.cleanRepeatId(); this.replaceHdImg(); await this.doFilter(); this.showNoItem(); this.bindClick().then(); new BroadcastChannel("channel-refresh").addEventListener("message", (async event2 => { "refresh" === event2.data.type && await this.doFilter(); })); $(this.getSelector().itemSelector + " a").attr("target", "_blank"); this.checkDom(); } checkDom() { if (!window.isListPage) return; const selector = this.getSelector(), targetNode = document.querySelector(selector.boxSelector), observer = new MutationObserver((mutations => { observer.disconnect(); try { this.replaceHdImg(); this.doFilter().then(); this.getBean("ListPageButtonPlugin").sortItems(); this.showNoItem(); this.getBean("CopyTitleOrDownImgPlugin").addCopy(); $(this.getSelector().itemSelector + " a").attr("target", "_blank"); } finally { observer.observe(targetNode, config); } })), config = { childList: !0, subtree: !1 }; observer.observe(targetNode, config); } showNoItem() { if ($(this.getSelector().itemSelector).length > 0) { 0 === $(this.getSelector().itemSelector).filter((function() { return "none" !== $(this).css("display"); })).length && show.info("当前内容已标记, 无内容可鉴定, 请进入下一页"); } } 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)), hasWatchCarNums = carList.filter((item => item.status === Status_HAS_WATCH)).map((item => item.carNum)); let hideFilterItem = await storageManager.getSetting("hideFilterItem", "yes"); isSearchPage && (hideFilterItem = "no"); movieList.forEach((ele => { let $box = $(ele); if (isJavBus && $box.find(".avatar-box").length > 0) return; const {carNum: carNum2, aHref: aHref, title: title} = this.findCarNumAndHref($box), hideKey = `${carNum2}-hide`, keywordHideKey = `${carNum2}-keywordHide`; if ("no" === hideFilterItem && $box.attr("data-hide") === hideKey) { $box.show(); $box.removeAttr("data-hide"); } if (filterKeywordList.some((filterKeyword => title.includes(filterKeyword) || carNum2.includes(filterKeyword))) && $box.attr("data-keyword-hide") !== keywordHideKey) { $box.hide(); $box.attr("data-keyword-hide", keywordHideKey); return; } if (filterCarNums.includes(carNum2) && "yes" === hideFilterItem && $box.attr("data-hide") !== hideKey) { $box.hide(); $box.attr("data-hide", hideKey); return; } if (hasDownCarNums.includes(carNum2) && "yes" === hideFilterItem && $box.attr("data-hide") !== hideKey) { $box.hide(); $box.attr("data-hide", hideKey); return; } if (hasWatchCarNums.includes(carNum2) && "yes" === hideFilterItem && $box.attr("data-hide") !== hideKey) { $box.hide(); $box.attr("data-hide", hideKey); return; } let tagText = "", color = ""; if (filterCarNums.includes(carNum2)) { tagText = "🚫 已屏蔽"; color = "#de3333"; } else if (favoriteCarNums.includes(carNum2)) { tagText = "⭐ 已收藏"; color = "#25b1dc"; } else if (hasDownCarNums.includes(carNum2)) { tagText = "📥️ 已下载"; color = "#7bc73b"; } else if (hasWatchCarNums.includes(carNum2)) { tagText = "🔍 已观看"; color = "#d7a80c"; } $box.find(".status-tag").remove(); if (tagText) { isJavDb && $box.find(".tags").append(`\n <span class="tag is-success status-tag" \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 status-tag" 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); } } })); $("#waitDownBtn span").text(`打开已收藏 (${favoriteCarNums.length})`); } async bindClick() { let selector = this.getSelector(); $(selector.boxSelector).on("click", ".item img", (async event2 => { event2.preventDefault(); event2.stopPropagation(); if ($(event2.target).closest("div.meta-buttons").length) return; const $box = $(event2.target).closest(".item"), {carNum: carNum2, aHref: aHref} = this.findCarNumAndHref($box); let dialogOpenDetail = await storageManager.getSetting("dialogOpenDetail", "yes"); if (carNum2.includes("FC2-")) { let movieId = this.parseMovieId(aHref); this.getBean("fc2Plugin").openFc2Dialog(movieId, carNum2, aHref); } else "yes" === dialogOpenDetail ? utils.openPage(aHref, carNum2, !1, event2) : window.open(aHref); })); $(selector.boxSelector).on("click", ".item video", (async event2 => { const video = event2.currentTarget; video.paused ? video.play().catch((e => console.error("播放失败:", e))) : video.pause(); event2.preventDefault(); event2.stopPropagation(); })); $(selector.boxSelector).on("click", ".item .video-title", (async event2 => { const $box = $(event2.target).closest(".item"), {carNum: carNum2, aHref: aHref} = this.findCarNumAndHref($box); if (carNum2.includes("FC2-")) { event2.preventDefault(); let movieId = this.parseMovieId(aHref); this.getBean("fc2Plugin").openFc2Dialog(movieId, carNum2, aHref); } })); $(selector.boxSelector).on("contextmenu", ".item img, .item video", (event2 => { event2.preventDefault(); const $box = $(event2.target).closest(".item"), {carNum: carNum2, aHref: aHref} = this.findCarNumAndHref($box); utils.q(event2, `是否屏蔽番号 ${carNum2}?`, (async () => { await storageManager.saveCar(carNum2, aHref, "", Status_FILTER); window.refresh(); show.ok("操作成功"); })); })); } findCarNumAndHref($box) { let carNum2, title, aHref = $box.find("a").attr("href"); if (isJavDb) { carNum2 = $box.find(".video-title").find("strong").text(); title = $box.find(".video-title").text(); } if (isJavBus) { carNum2 = aHref.split("/").filter(Boolean).pop(); title = $box.find("img").attr("title"); } if (!carNum2) { const msg = "提取番号信息失败"; show.error(msg); throw new Error(msg); } return { carNum: carNum2, 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; } 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"); isJavBus && $(dom).find(".avatar-box").length > 0 && $(dom).find(".avatar-box").parent().remove(); let itemList = dom.querySelectorAll(this.getSelector().requestDomItemSelector), pagination = dom.querySelectorAll(".pagination"); const listPagePlugin = this.getBean("listPagePlugin"); await listPagePlugin.filterMovieList(itemList); let coverImgNodeList = dom.querySelectorAll(this.getSelector().coverImgSelector); listPagePlugin.replaceHdImg(coverImgNodeList); let $movieList = $(this.getSelector().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", "/advanced_search?type=100" ].some((path => window.location.href.includes(path))); } async handlePaging() { if (this.shouldDisablePaging()) return; if ($("#no-page").length) { $("#auto-page").remove(); return; } if (0 === $(this.getSelector().boxSelector).length) return; if (!window.isListPage) return; if (this.paging) return; let needPaging = !0; $(`${this.getSelector().itemSelector}:visible`).each(((i, el) => { 0 === $(el).find("span:contains(🚫 已屏蔽)").length && 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: 100, callback: () => { nextBtn[0].click(); this.paging = !1; } }); } } } class HighlightMagnetPlugin extends BasePlugin { handle() { this.handleDb(); this.handleBus(); } handleDb() { let magnetNameList = $("#magnets-content .name").toArray(); if (0 === magnetNameList.length) return; let has4k_C_UC = !1; magnetNameList.forEach((el => { let item = $(el), text = item.text().toLowerCase(); text.includes("4k") && item.css("color", "#f40"); (text.includes("-c") || text.includes("-u") || text.includes("-uc") || text.includes("4k")) && (has4k_C_UC = !0); })); has4k_C_UC ? magnetNameList.forEach((el => { let item = $(el), text = item.text().toLowerCase(); text.includes("-c") || text.includes("-u") || text.includes("-uc") || text.includes("4k") || 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.includes("4k") && item.css("color", "#f40"); (text.includes("-c") || text.includes("-u") || text.includes("-uc") || text.includes("4k")) && (has4k_C_UC = !0); })); has4k_C_UC ? magnetNameList.forEach((el => { let item = $(el), text = item.text().toLowerCase(); text.includes("-c") || text.includes("-u") || text.includes("-uc") || text.includes("4k") || 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(); return await gmHttp.post(url, data, headers); } 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", "JHS-数据备份"); } getDefaultGridColumns() { return window.innerWidth < 600 ? 1 : window.innerWidth < 900 ? 2 : window.innerWidth < 1e3 ? 3 : window.innerWidth < 1200 ? 4 : 5; } async initCss() { let containerWidth = await storageManager.getSetting("containerWidth", "100"), containerColumns = await storageManager.getSetting("containerColumns", this.getDefaultGridColumns()), 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 font-size: 14px;\n min-width: 180px;\n font-weight: bold;\n margin-right: 10px;\n }\n .form-content{\n max-width: 160px;\n min-width: 160px;\n }\n .form-content * {\n width: 100%;\n padding: 5px;\n margin-right: 10px;\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,#moreBtn {\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 #moreBtn{\n background-color: #ad8731;\n color: white;\n }\n #moreBtn:hover {\n background-color: #dc9f11;\n }\n \n .simple-setting, .mini-simple-setting {\n display: none; /* 默认隐藏 */\n background: rgba(255,255,255,0.9); \n position: absolute;\n top: 35px; /* 在按钮正下方显示 */\n right: -200%;\n z-index: 1000;\n border: 1px solid #ddd;\n border-radius: 4px;\n box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);\n padding: 0;\n margin-top: 5px; /* 稍微拉开一点距离 */\n color: #363131;\n }\n \n .mini-switch {\n appearance: none;\n -webkit-appearance: none;\n width: 40px;\n height: 20px;\n background: #e0e0e0;\n border-radius: 20px;\n position: relative;\n cursor: pointer;\n outline: none;\n /*transition: all 0.2s ease;*/\n }\n \n .mini-switch:checked {\n background: #4CAF50;\n }\n \n .mini-switch::before {\n content: "";\n position: absolute;\n width: 16px;\n height: 16px;\n border-radius: 50%;\n background: white;\n top: 2px;\n left: 2px;\n box-shadow: 0 1px 3px rgba(0,0,0,0.2);\n /*transition: all 0.2s ease;*/\n }\n \n .mini-switch:checked::before {\n left: calc(100% - 18px);\n }\n </style\n `; } handle() { if (isJavDb) { let handleResize = function() { if ($(".navbar-search").is(":hidden")) { $(".mini-setting-box").hide(); $(".setting-box").show(); } else { $(".mini-setting-box").show(); $(".setting-box").hide(); } }; $("#navbar-menu-user .navbar-end").prepend('<div class="navbar-item has-dropdown is-hoverable setting-box" style="position:relative;">\n <a id="setting-btn" class="navbar-link nav-btn" style="color: #ff8400 !important;padding-right:15px !important;">\n 设置\n </a>\n <div class="simple-setting"></div>\n </div>'); utils.loopDetector((() => $("#miniHistoryBtn").length > 0), (() => { $(".miniHistoryBtnBox").before('\n <div class="navbar-item mini-setting-box" style="position:relative;margin-left: auto;">\n <a id="mini-setting-btn" class="navbar-link nav-btn" style="color: #ff8400 !important;padding-left:0 !important;padding-right:0 !important;">\n 设置\n </a>\n <div class="mini-simple-setting"></div>\n </div>\n '); handleResize(); })); $(window).resize(handleResize); } isJavBus && $("#navbar").append(`\n <ul class="nav navbar-nav navbar-right setting-box">\n <li><a id="setting-btn" style="color: #ff8400 !important;padding-right:15px !important;" role="button">设置</a><div class="simple-setting">${this.simpleSetting()}</div></li>\n </ul>\n `); $(".main-nav, .top-bar").on("click", "#setting-btn, #mini-setting-btn", (() => { 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="videoQuality">\n <option value="mmb">中画质 (432p)</option>\n <option value="mhb">高画质 (576p)</option>\n <option value="hmb">HD (720p)</option>\n <option value="hhb">FullHD (1080p)</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">\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">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: utils.getResponsiveArea(), scrollbar: !1, success: (layero, index) => { $(layero).find(".layui-layer-content").css("position", "relative"); this.loadForm(); this.bindClick(); } }); })); $(".main-nav, .top-bar").on("mouseenter", ".setting-box", (() => { $(".simple-setting").html(this.simpleSetting()).show(); this.initSimpleSettingForm().then(); })).on("mouseleave", ".setting-box", (() => { $(".simple-setting").html("").hide(); })); $(".main-nav, .top-bar").on("mouseenter", ".mini-setting-box", (() => { $(".mini-simple-setting").html(this.simpleSetting()).show(); this.initSimpleSettingForm().then(); })).on("mouseleave", ".mini-setting-box", (() => { $(".mini-simple-setting").html("").hide(); })); } simpleSetting() { return '\n <div style="display: flex; flex-direction: column; height: 100%;margin-top:20px">\n <div style=" flex: 1; overflow-y: auto; margin: 0 10px; ">\n <div class="setting-item">\n <span class="setting-label">隐藏已鉴定内容:</span>\n <div class="form-content">\n <input type="checkbox" id="hideFilterItem" class="mini-switch">\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">弹窗方式打开页面:</span>\n <div class="form-content">\n <input type="checkbox" id="dialogOpenDetail" class="mini-switch">\n </div>\n </div> \n \n <div class="setting-item">\n <span class="setting-label">页面列数: <span id="showContainerColumns"></span></span>\n <div class="form-content">\n <input type="range" id="containerColumns" min="3" max="10" step="1" style="padding:5px 0">\n </div>\n </div>\n \n <div class="setting-item">\n <span class="setting-label">页面宽度: <span id="showContainerWidth"></span></span>\n <div class="form-content">\n <input type="range" id="containerWidth" min="0" max="30" step="1" style="padding:5px 0">\n </div>\n </div>\n </div>\n <div style="flex-shrink: 0; padding: 0 20px 15px; text-align: right; border-top: 1px solid #eee;"> \n <button id="moreBtn">更多设置</button>\n </div>\n </div>\n '; } async loadForm() { let settingObj = await storageManager.getSetting(); $("#videoQuality").val(settingObj.videoQuality || "hhb"); $("#reviewCount").val(settingObj.reviewCount || 20); $("#waitCheckCount").val(settingObj.waitCheckCount || 5); $("#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); })); })); } async initSimpleSettingForm() { let settingObj = await storageManager.getSetting(); $("#hideFilterItem").prop("checked", !settingObj.hideFilterItem || "yes" === settingObj.hideFilterItem); $("#containerColumns").val(settingObj.containerColumns || 4); $("#showContainerColumns").text(settingObj.containerColumns || 4); $("#containerWidth").val((settingObj.containerWidth || 100) - 70); $("#showContainerWidth").text((settingObj.containerWidth || 100) + "%"); $("#dialogOpenDetail").prop("checked", !settingObj.dialogOpenDetail || "yes" === settingObj.dialogOpenDetail); $("#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))`; } storageManager.saveSettingItem("containerColumns", columns); })); $("#containerWidth").on("input", (event2 => { let containerWidth = parseInt($(event2.target).val()); const value = containerWidth + 70 + "%"; $("#showContainerWidth").text(value); if (isJavDb) { document.querySelector("section .container").style.minWidth = value; } if (isJavBus) { document.querySelector(".container-fluid .row").style.minWidth = value; } storageManager.saveSettingItem("containerWidth", containerWidth + 70); })); $("#dialogOpenDetail").on("change", (event2 => { let dialogOpenDetail = $("#dialogOpenDetail").is(":checked") ? "yes" : "no"; storageManager.saveSettingItem("dialogOpenDetail", dialogOpenDetail); })); $("#hideFilterItem").on("change", (event2 => { let hideFilterItem = $("#hideFilterItem").is(":checked") ? "yes" : "no"; storageManager.saveSettingItem("hideFilterItem", hideFilterItem); window.refresh(); })); $("#moreBtn").on("click", (() => { $(".simple-setting").html("").hide(); $("#setting-btn")[0].click(); })); } 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); } }))); $("#saveBtn").on("click", (() => this.saveForm())); } async saveForm() { let settingObj = await storageManager.getSetting(); settingObj.videoQuality = $("#videoQuality").val(); settingObj.reviewCount = $("#reviewCount").val(); settingObj.waitCheckCount = $("#waitCheckCount").val(); settingObj.refresh_token = $("#refresh_token").val(); settingObj.webDavUrl = $("#webDavUrl").val(); settingObj.webDavUsername = $("#webDavUsername").val(); settingObj.webDavPassword = $("#webDavPassword").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(); this.handleVideo().then(); })); window.location.href.includes("autoPlay=1") && $preview[0].click(); } async handleVideo() { if ($("#preview-video").length > 0) return; let carNum2 = this.getPageInfo().carNum; const dmmVideoMap = await getDmmVideo(carNum2); if (!dmmVideoMap) return null; await this.createQualityBtn(dmmVideoMap); const element = document.getElementById("preview-video"); if (element) { const rect = element.getBoundingClientRect(); window.scrollTo({ top: window.scrollY + rect.top - 100, behavior: "smooth" }); } } async createQualityBtn(dmmVideoMap) { let defaultVideoQuality = await storageManager.getSetting("videoQuality") || "hhb"; dmmVideoMap[defaultVideoQuality] || (defaultVideoQuality = Object.keys(dmmVideoMap)[0]); let defaultVideoUrl = dmmVideoMap[defaultVideoQuality]; $("#magneturlpost").next().after(`<div><video id="preview-video" controls style="width: 100%;margin-top: 5px;"><source src="${defaultVideoUrl}" /></video></div>`); 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(); let buttonsHtml = ""; [ { id: "video-mmb", quality: "mmb", text: "中画质 (432p)" }, { id: "video-mhb", quality: "mhb", text: "高画质 (576p)" }, { id: "video-hmb", quality: "hmb", text: "HD (720p)" }, { id: "video-hhb", quality: "hhb", text: "FullHD (1080p)" } ].forEach(((option, index) => { let dmmVideoUrl = dmmVideoMap[option.quality]; if (dmmVideoUrl) { const isActive = defaultVideoQuality === option.quality; buttonsHtml += `\n <button class="video-control-btn${isActive ? " active" : ""}" \n id="${option.id}" \n data-quality="${option.quality}"\n data-video-src = "${dmmVideoUrl}"\n style="bottom: ${40 * index}px; right: -105px;">\n ${option.text}\n </button>\n `; } })); $videoContainer.append(buttonsHtml); const $buttons = $videoContainer.find(".video-control-btn"); $videoContainer.on("click", ".video-control-btn", (async e => { try { const $button = $(e.currentTarget); if ($button.hasClass("active")) return; let videoSrc2 = $button.attr("data-video-src"); $previewSource.attr("src", videoSrc2); videoEl.load(); videoEl.muted = !1; await videoEl.play(); $buttons.removeClass("active"); $button.addClass("active"); } catch (error) { show.error("切换画质失败"); console.error("切换画质失败:", error); } })); } } class SearchByImagePlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "siteList", [ { name: "TinEye", url: "https://tineye.com/search?url={占位符}", ico: "https://www.google.com/s2/favicons?sz=64&domain=tineye.com" }, { name: "Bing", url: "https://www.bing.com/images/search?q=imgurl:{占位符}&view=detailv2&iss=sbi", ico: "https://www.bing.com/favicon.ico" }, { 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" } ]); __publicField(this, "isUploading", !1); } 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: utils.isMobile() ? utils.getResponsiveArea() : [ "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", (async () => { const imageSrc = $previewImage.attr("src"); if (imageSrc) { if (!this.isUploading) { this.isUploading = !0; try { const imgUrl = await this.searchByImage(imageSrc); $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(); } finally { this.isUploading = !1; } } } else 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"; $("#handle-btn")[0].click(); }; 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); __publicField(this, "isInit", !1); } async showRelated($eleBox) { const $magnets = $eleBox || $("#magnets-content"); let movieId = this.parseMovieId(window.location.href); $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 '); $("#relatedFold").on("click", (event2 => { event2.preventDefault(); event2.stopPropagation(); const $text = $("#relatedFold .toggle-text"), $icon = $("#relatedFold .toggle-icon"), isFolded = "展开" === $text.text(); $text.text(isFolded ? "折叠" : "展开"); $icon.text(isFolded ? "▲" : "▼"); if (isFolded) { $("#relatedContainer").show(); $("#relatedFooter").show(); if (!this.isInit) { this.fetchAndDisplayRelateds(movieId); this.isInit = !0; } } else { $("#relatedContainer").hide(); $("#relatedFooter").hide(); } })); $magnets.append('<div id="relatedContainer"></div>'); $magnets.append('<div id="relatedFooter"></div>'); } async fetchAndDisplayRelateds(movieId) { const $relatedContainer = $("#relatedContainer"), $relatedFooter = $("#relatedFooter"); $relatedContainer.append('<div id="relatedLoading" style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">获取清单中...</div>'); let dataList = null; try { dataList = await related(movieId, 1, 20); } catch (e) { console.error("获取清单失败:", e); } finally { $("#relatedLoading").remove(); } if (dataList) if (0 !== dataList.length) { this.displayRelateds(dataList, $relatedContainer); if (20 === dataList.length) { $relatedFooter.html('\n <button id="loadMoreRelateds" 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="relatedEnd" style="display:none; text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部清单</div>\n '); let currentPage = 1, $loadMoreRelateds = $("#loadMoreRelateds"); $loadMoreRelateds.on("click", (async () => { $loadMoreRelateds.text("加载中...").prop("disabled", !0); currentPage++; let moreData; try { moreData = await related(movieId, currentPage, 20); } catch (e) { console.error("加载更多清单失败:", e); } finally { $loadMoreRelateds.text("加载失败, 请点击重试").prop("disabled", !1); } if (moreData) { this.displayRelateds(moreData, $relatedContainer); if (moreData.length < 20) { $loadMoreRelateds.remove(); $("#relatedEnd").show(); } else $loadMoreRelateds.text("加载更多清单").prop("disabled", !1); } })); } else $relatedFooter.html('<div style="text-align:center; padding:10px; color:#666; margin-top:10px;">已加载全部清单</div>'); } else $relatedContainer.append('<div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">无清单</div>'); else { $relatedContainer.append('\n <div style="margin-top:15px;background-color:#ffffff;padding:10px;margin-left: -10px;">\n 获取清单失败\n <a id="retryFetchRelateds" href="javascript:;" style="margin-left: 10px; color: #1890ff; text-decoration: none;">重试</a>\n </div>\n '); $("#retryFetchRelateds").on("click", (async () => { $("#retryFetchRelateds").parent().remove(); await this.fetchAndDisplayRelateds(movieId); })); } } displayRelateds(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.movieCount}</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;">导入至 JHS</a>'); $("#wantWatchBtn").on("click", (event2 => { this.type = Status_FAVORITE; this.importWantWatchVideos(event2, "是否将 想看的影片 导入到 JHS-收藏?"); })); } if (window.location.href.includes("/watched_videos")) { $("h3").append('<a class="a-success" id="wantWatchBtn" style="padding:10px;">导入至 JHS</a>'); $("#wantWatchBtn").on("click", (event2 => { this.type = Status_HAS_DOWN; this.importWantWatchVideos(event2, "是否将 看过的影片 导入到 JHS-已下载?"); })); } } 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"), carNum2 = item.find(".video-title strong").text().trim(); if (href && carNum2) try { if (await storageManager.getCar(carNum2)) { show.info(`${carNum2} 已存在, 跳过`); continue; } await storageManager.saveCar(carNum2, href, "", this.type); } catch (error) { console.error(`保存失败 [${carNum2}]:`, 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 SeHuaTangPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "currentImageIndex", 0); __publicField(this, "currentImageGroup", []); __publicField(this, "processedArticles", new Set); } async initCss() { return "\n <style>\n /*.icn{\n width: 85px !important;\n }*/\n .xst{\n font-size: 15px;\n color: #090909;\n }\n #threadlisttableid em{\n font-size: 15px;\n }\n </style>\n "; } async handle() { let $enter = $(".enter-btn"); $enter.length > 0 && $enter[0].click(); if (!window.location.href.includes("viewthread")) { this.parseArticleImg().then(); this.checkDom(); this.bindClick(); this.handleImg(); } } getInfo($tr) { let articleId, $a = $tr.find("a.xst"), title = $a.text().trim(), url = $a.attr("href"); articleId = url.includes("tid=") ? url.match(/tid=(\d+)/)[1] : url.split("-")[1]; return { articleId: articleId, url: url, title: title }; } bindClick() { $("#threadlisttableid").on("click", ".block-btn", (async () => { let $row = $(event.target).closest("tr"); const {articleId: articleId, url: url, title: title} = this.getInfo($row); await seHuaTangStorageManager.saveArticle(articleId, url, title, Status_FILTER); show.error("屏蔽成功!"); this.doFilter().then(); })).on("click", ".fav-btn", (async event2 => { let $row = $(event2.target).closest("tr"); const {articleId: articleId, url: url, title: title} = this.getInfo($row); await seHuaTangStorageManager.saveArticle(articleId, url, title, Status_FAVORITE); show.ok("收藏成功!"); this.doFilter().then(); })); } async doFilter() { const articleList = await seHuaTangStorageManager.getArticleList(); $('.icn a[title="新窗口打开"], .icn a[title="有新回复 - 新窗口打开"]').toArray().forEach((item => { $(item).hide(); let $td = $(item).parent(); $td.find(".fav-btn").length || $td.prepend('\n <a class="block-btn" style="color: #d99c1c; cursor: pointer; display: inline-block;min-width: 37px;">屏蔽</a>\n <a class="fav-btn" style="color: #1cd925; cursor: pointer; display: inline-block;min-width: 37px;">收藏</a>\n '); let $tr = $td.parent(); const {articleId: articleId, url: url, title: title} = this.getInfo($tr), article = articleList.find((item2 => item2.articleId === articleId)); article && article.status === Status_FAVORITE && $tr.find(".common em a").text("已收藏").css("color", "#14e097"); if (article && article.status === Status_FILTER) { $tr.find(".common em a").text("已屏蔽").css("color", "#c72222"); $tr.parent().hide(); } })); } checkDom() { const targetNode = document.querySelector("#threadlisttableid"), observer = new MutationObserver((async mutations => { observer.disconnect(); try { await this.doFilter(); this.parseArticleImg().then(); } finally { observer.observe(targetNode, config); } })), config = { childList: !0, subtree: !1 }; observer.observe(targetNode, config); } async parseArticleImg() { $(".s.xst").each((async (index, ele) => { const articleUrl = $(ele).attr("href"); if (!this.processedArticles.has(articleUrl)) { this.processedArticles.add(articleUrl); try { const $tbody = $(ele).closest("tbody"); if ($tbody.find(".imageBox").length) return; if (!$tbody.is(":visible")) return; const res = await fetch(articleUrl); if (!res.ok) return; const imgs = $($.parseHTML(await res.text())).find("img.zoom[file]:not([file*='static'], [file*='hrline'])").slice(0, 5); if (!imgs.length) return; const imgHTML = imgs.map(((_, img) => `<img src="${$(img).attr("file")}" style="width:300px;height:auto;max-width:300px;max-height:300px;object-fit:contain" onclick="zoom(this,this.src,0,0,0)" alt="">`)).get().join(""); $tbody.append(`\n <tr class="imageBox">\n <td colspan="5">\n <div style="display:flex;gap:10px;overflow-x:auto;padding:5px 0">${imgHTML}</div>\n </td>\n </tr>\n `); } catch (e) { console.error("Error:", articleUrl, e); } } })); } handleImg() { document.addEventListener("click", (event2 => { if ("IMG" === event2.target.tagName && event2.target.closest(".imageBox")) { const previewTbody = event2.target.closest(".imageBox"); this.currentImageGroup = Array.from(previewTbody.querySelectorAll("img")); this.currentImageIndex = this.currentImageGroup.indexOf(event2.target); this.createNavigateBtn(); } })); } navigateImage(direction) { this.currentImageIndex = (this.currentImageIndex + direction + this.currentImageGroup.length) % this.currentImageGroup.length; const img = this.currentImageGroup[this.currentImageIndex]; zoom(img, img.src, 0, 0, 0); this.createNavigateBtn(); } createNavigateBtn() { utils.loopDetector((() => $("#imgzoom_picpage").length > 0), (() => { if (0 === $("#imgzoom_picpage").length) return; const zoomContainer = document.getElementById("imgzoom_picpage"); console.log("zoomContainer", zoomContainer); if (!zoomContainer) return; zoomContainer.querySelectorAll("#zimg_prev, #zimg_next").forEach((btn => btn.remove())); const prevBtn = document.createElement("div"); prevBtn.id = "zimg_prev"; prevBtn.className = "zimg_prev"; prevBtn.onclick = () => this.navigateImage(-1); const nextBtn = document.createElement("div"); nextBtn.id = "zimg_next"; nextBtn.className = "zimg_next"; nextBtn.onclick = () => this.navigateImage(1); zoomContainer.append(prevBtn, nextBtn); })); } } class CopyTitleOrDownImgPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "moreSvg", '<svg t="1749017229420" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="9184" width="200" height="200"><path d="M512 74.666667C270.933333 74.666667 74.666667 270.933333 74.666667 512S270.933333 949.333333 512 949.333333 949.333333 753.066667 949.333333 512 753.066667 74.666667 512 74.666667z m0 810.666666c-204.8 0-373.333333-168.533333-373.333333-373.333333S307.2 138.666667 512 138.666667 885.333333 307.2 885.333333 512 716.8 885.333333 512 885.333333z" fill="#666666" p-id="9185"></path><path d="M512 512m-42.666667 0a42.666667 42.666667 0 1 0 85.333334 0 42.666667 42.666667 0 1 0-85.333334 0Z" fill="#666666" p-id="9186"></path><path d="M341.333333 512m-42.666666 0a42.666667 42.666667 0 1 0 85.333333 0 42.666667 42.666667 0 1 0-85.333333 0Z" fill="#666666" p-id="9187"></path><path d="M682.666667 512m-42.666667 0a42.666667 42.666667 0 1 0 85.333333 0 42.666667 42.666667 0 1 0-85.333333 0Z" fill="#666666" p-id="9188"></path></svg>'); __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>'); __publicField(this, "videoSvg", '<svg t="1749003664455" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1952" width="200" height="200"><path d="M825.6 153.6H198.4C124.5 153.6 64 214.1 64 288v448c0 73.9 60.5 134.4 134.4 134.4h627.2c73.9 0 134.4-60.5 134.4-134.4V288c0-73.9-60.5-134.4-134.4-134.4z m-138.2 44.8l112 112H706l-112-112h93.4z m-156.8 0l112 112H526.7l-112-112h115.9z m-179.2 0l112 112H347.5l-112-112h115.9zM108.8 288c0-41.4 28.4-76.1 66.7-86.3l108.7 108.7H108.8V288z m806.4 448c0 49.4-40.2 89.6-89.6 89.6H198.4c-49.4 0-89.6-40.2-89.6-89.6V355.2h806.4V736z m0-425.6h-52.5l-112-112h74.9c49.4 0 89.6 40.2 89.6 89.6v22.4z" p-id="1953"></path><path d="M454 687.2l149.3-77.6c27.5-13.8 27.5-53 0-66.8L468 472.2c-31.2-15.6-68 7.1-68 42v139.6c0 27.8 29.2 45.8 54 33.4zM444.8 512l134.4 67.2-134.4 67.2V512z" p-id="1954"></path></svg>'); __publicField(this, "recoveryVideoSvg", '<svg t="1749003779161" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8204" width="200" height="200"><path d="M938.666667 553.92V768c0 64.8-52.533333 117.333333-117.333334 117.333333H202.666667c-64.8 0-117.333333-52.533333-117.333334-117.333333V256c0-64.8 52.533333-117.333333 117.333334-117.333333h618.666666c64.8 0 117.333333 52.533333 117.333334 117.333333v297.92z m-64-74.624V256a53.333333 53.333333 0 0 0-53.333334-53.333333H202.666667a53.333333 53.333333 0 0 0-53.333334 53.333333v344.48A290.090667 290.090667 0 0 1 192 597.333333a286.88 286.88 0 0 1 183.296 65.845334C427.029333 528.384 556.906667 437.333333 704 437.333333c65.706667 0 126.997333 16.778667 170.666667 41.962667z m0 82.24c-5.333333-8.32-21.130667-21.653333-43.648-32.917333C796.768 511.488 753.045333 501.333333 704 501.333333c-121.770667 0-229.130667 76.266667-270.432 188.693334-2.730667 7.445333-7.402667 20.32-13.994667 38.581333-7.68 21.301333-34.453333 28.106667-51.370666 13.056-16.437333-14.634667-28.554667-25.066667-36.138667-31.146667A222.890667 222.890667 0 0 0 192 661.333333c-14.464 0-28.725333 1.365333-42.666667 4.053334V768a53.333333 53.333333 0 0 0 53.333334 53.333333h618.666666a53.333333 53.333333 0 0 0 53.333334-53.333333V561.525333zM320 480a96 96 0 1 1 0-192 96 96 0 0 1 0 192z m0-64a32 32 0 1 0 0-64 32 32 0 0 0 0 64z" fill="#000000" p-id="8205"></path></svg>'); } async initCss() { return `\n <style>\n .box .tags {\n justify-content: space-between;\n }\n .tool-box span{\n opacity:.3\n }\n .tool-box span:hover{\n opacity:1\n }\n ${isJavBus ? ".tool-box .icon{ height: 2rem; width: 2rem; }" : ""}\n .tool-box svg path {\n fill: blue;\n }\n [data-theme="dark"] .tool-box svg path {\n fill: white;\n }\n \n \n /* 鼠标移入时的弹性动画 */\n .elastic-in {\n animation: elasticIn 0.2s ease-out forwards; /* 动画名称 | 时长 | 缓动函数 | 保持最终状态 */\n }\n \n /* 鼠标移出时的弹性动画 */\n .elastic-out {\n animation: elasticOut 0.2s ease-in forwards;\n }\n /* 弹性进入动画(像果冻弹入) */\n @keyframes elasticIn {\n 0% {\n opacity: 0;\n transform: scale(0.8); /* 起始状态:80% 大小 */\n }\n 50% {\n opacity: 1;\n transform: scale(1.1); /* 弹到 110%(超调一点) */\n }\n 70% {\n transform: scale(0.95); /* 回弹到 95%(模拟弹性阻尼) */\n }\n 100% {\n opacity: 1;\n transform: scale(1); /* 最终恢复正常大小 */\n }\n }\n /* 弹性离开动画(像果冻弹出) */\n @keyframes elasticOut {\n 0% {\n opacity: 1;\n transform: scale(1); /* 起始状态:正常大小 */\n }\n 30% {\n transform: scale(1.05); /* 先弹大一点(105%) */\n }\n 100% {\n opacity: 0;\n transform: scale(0.8); /* 最终缩小并消失 */\n }\n }\n \n \n .loading {\n opacity: 0.7;\n filter: blur(1px);\n }\n .loading-spinner {\n position: absolute;\n top: 50%;\n left: 50%;\n transform: translate(-50%, -50%);\n width: 40px;\n height: 40px;\n border: 3px solid rgba(255,255,255,.3);\n border-radius: 50%;\n border-top-color: #fff;\n animation: spin 1s ease-in-out infinite;\n z-index: 20;\n }\n @keyframes spin {\n to { transform: translate(-50%, -50%) rotate(360deg); }\n }\n </style>\n `; } handle() { if (window.isListPage) { this.addCopy(); this.bindClick(); } } addCopy() { $(this.getSelector().itemSelector).toArray().forEach((ele => { let $box = $(ele); if (!($box.find(".tool-box").length > 0)) { isJavDb && $box.find(".tags").append(`\n <div class="tool-box" style="margin-left: auto; display: flex; align-items: center">\n <span class="videoSvg" title="播放视频" style="margin-right: 15px;">${this.videoSvg}</span>\n \n <div class="more-tools-container" style="position: relative; margin-right: 15px;">\n \x3c!-- 主按钮 --\x3e\n <div style="padding: 5px; margin: -5px;opacity:.3">${this.moreSvg}</div>\n \n \x3c!-- 下拉菜单 --\x3e\n <div class="more-tools" style="\n position: absolute;\n bottom: 20px;\n right: -10px;\n display: none;\n background: white;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n border-radius: 20px;\n padding: 10px 0;\n margin-bottom: 15px; /* 增加底部间距 防止鼠标容易移出*/\n z-index: 10;\n ">\n <span class="carNumSvg" title="复制番号" style="padding: 5px 10px; white-space: nowrap;">${this.carNumSvg}</span>\n <span class="titleSvg" title="复制标题" style="padding: 5px 10px; white-space: nowrap;">${this.titleSvg}</span>\n <span class="downSvg" title="下载封面" style="padding: 5px 10px; white-space: nowrap;">${this.downSvg}</span>\n </div>\n </div>\n </div>\n `); if (isJavBus) { if ($box.find(".avatar-box").length > 0) return; $box.find(".photo-info").append(`\n <div class="tool-box" style="display: flex; align-items: center;justify-content: flex-end">\n <span class="videoSvg" title="播放视频" style="margin-right: 15px;">${this.videoSvg}</span>\n \n <div class="more-tools-container" style="position: relative;">\n \x3c!-- 主按钮 --\x3e\n <div style="padding: 5px; margin: -5px;opacity:.3">${this.moreSvg}</div>\n \n \x3c!-- 下拉菜单 --\x3e\n <div class="more-tools" style="\n position: absolute;\n bottom: 20px;\n right: -10px;\n display: none;\n background: white;\n box-shadow: 0 2px 8px rgba(0,0,0,0.15);\n border-radius: 20px;\n padding: 10px 0;\n margin-bottom: 15px; /* 增加底部间距 防止鼠标容易移出*/\n z-index: 10;\n ">\n <span class="carNumSvg" title="复制番号" style="padding: 5px 10px; white-space: nowrap;">${this.carNumSvg}</span>\n <span class="titleSvg" title="复制标题" style="padding: 5px 10px; white-space: nowrap;">${this.titleSvg}</span>\n <span class="downSvg" title="下载封面" style="padding: 5px 10px; white-space: nowrap;">${this.downSvg}</span>\n </div>\n </div>\n </div>\n `); } $box.find(".more-tools-container").on({ mouseenter: function() { $(this).find(".more-tools").stop(!0, !0).removeClass("elastic-out").addClass("elastic-in").show(); }, mouseleave: function() { $(this).find(".more-tools").stop(!0, !0).removeClass("elastic-in").addClass("elastic-out").delay(200).queue((function(next) { $(this).hide(); next(); })); } }); } })); } bindClick() { const selector = this.getSelector(), listPagePlugin = this.getBean("ListPagePlugin"); $(document).on("click", ".titleSvg", (event2 => { event2.preventDefault(); event2.stopPropagation(); const $box = $(event2.target).closest(".item"), {carNum: carNum2, 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: carNum2, aHref: aHref, title: title} = listPagePlugin.findCarNumAndHref($box); navigator.clipboard.writeText(carNum2).then((() => { show.info("番号已复制到剪切板, " + carNum2); })).catch((err => { console.error("复制失败: ", err); })); })).on("click", ".downSvg", (event2 => { event2.preventDefault(); event2.stopPropagation(); const $box = $(event2.target).closest(".item"), {carNum: carNum2, 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"); http.get(url).then((content => { utils.download(content, title + ".jpg"); })); })).on("click", ".videoSvg", (event2 => { event2.preventDefault(); event2.stopPropagation(); $('.videoSvg[title!="播放视频"]').each(((index, element) => { const $otherSvgElement = $(element); let $otherBox = $otherSvgElement.closest(".item"), $otherImg = $otherBox.find(selector.coverImgSelector), {carNum: carNum2} = listPagePlugin.findCarNumAndHref($otherBox); this.showImg($otherSvgElement, $otherImg, carNum2); $otherSvgElement.html(this.videoSvg).attr("title", "播放视频"); })); const $currentBox = $(event2.target).closest(".item"), $svgElement = $currentBox.find(".videoSvg"); if ("播放视频" === $svgElement.attr("title")) { $svgElement.html(this.recoveryVideoSvg).attr("title", "切回封面"); const {carNum: carNum2} = listPagePlugin.findCarNumAndHref($currentBox); let $img = $currentBox.find(selector.coverImgSelector); $img.length || show.error("没有找到图片"); this.showVideo($svgElement, $img, carNum2).then(); } })); } showImg($svgElement, $img, carNum2) { $svgElement.html(this.videoSvg).attr("title", "播放视频"); let $video = $(`#${`${carNum2}_preview_video`}`); if ($video.length > 0) { $video[0].pause(); $video.hide(); } $img.show(); $img.removeClass("loading"); $img.next(".loading-spinner").remove(); } async showVideo($svgElement, $img, carNum2) { const id = `${carNum2}_preview_video`; let $video = $(`#${id}`); if ($video.length > 0) { $video.show(); $video[0].play(); $img.hide(); return; } $img.addClass("loading"); $img.after('<div class="loading-spinner"></div>'); const poster = $img.attr("src"), dmmVideoMap = await getDmmVideo(carNum2); if (!dmmVideoMap) { show.error("获取预览视频地址失败"); this.showImg($svgElement, $img, carNum2); return; } let defaultVideoQuality = await storageManager.getSetting("videoQuality") || "hhb"; dmmVideoMap[defaultVideoQuality] || (defaultVideoQuality = Object.keys(dmmVideoMap)[0]); const videoHtml = `\n <div style="display: flex; justify-content: center; align-items: center; position: absolute; top:0; left:0; height: 100%; width: 100%; z-index: 10; overflow: hidden">\n <video \n src="${dmmVideoMap[defaultVideoQuality]}" \n poster="${poster}" \n id="${id}" \n controls \n loop \n muted \n playsinline\n style="max-height: 100%; max-width: 100%; object-fit: contain"\n ></video>\n </div>\n `; $img.parent().append(videoHtml); $img.hide(); $img.removeClass("loading"); $img.next(".loading-spinner").remove(); $video = $(`#${id}`); let videoElement = $video[0]; videoElement.load(); videoElement.muted = !1; videoElement.play(); $video.trigger("focus"); } } class Fc2By123AvPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "baseUrl", "https://123av.com/ja"); __publicField(this, "$contentBox", $(".section .container")); __publicField(this, "urlParams", new URLSearchParams(window.location.search)); __publicField(this, "sortVal", this.urlParams.get("sort") || "release_date"); __publicField(this, "currentPage", this.urlParams.get("page") ? parseInt(this.urlParams.get("page")) : 1); __publicField(this, "maxPage", null); __publicField(this, "keyword", this.urlParams.get("keyword") || null); } handle() { $('.tabs li:contains("FC2")').after('<li><a href="/advanced_search?type=100&released_start=2099-09"><span>123Av-Fc2</span></a></li>'); if (currentHref.includes("/advanced_search?type=100")) { this.hookPage(); this.handleQuery().then(); } } hookPage() { let $h2 = $("h2.section-title"); $h2.contents().first().replaceWith("123Av"); $h2.css("marginBottom", "0"); $h2.append('\n <div style="margin-left: 100px; width: 400px;">\n <input id="search-123av-keyword" type="text" placeholder="搜索123Av Fc2ppv内容" style="padding: 4px 5px;margin-right: 0">\n <a id="search-123av-btn" class="a-primary" style="margin-left: 0">搜索</a>\n <a id="clear-123av-btn" class="a-dark" style="margin-left: 0">重置</a>\n </div>\n '); $("#search-123av-keyword").val(this.keyword); $("#search-123av-btn").on("click", (async () => { let keyword = $("#search-123av-keyword").val().trim(); if (keyword) { this.keyword = keyword; utils.setHrefParam("keyword", keyword); await this.handleQuery(); } })); $("#clear-123av-btn").on("click", (async () => { $("#search-123av-keyword").val(""); this.keyword = ""; utils.setHrefParam("keyword", ""); $(".page-box").show(); $(".tool-box").show(); await this.handleQuery(); })); $(".empty-message").remove(); $("#foldCategoryBtn").remove(); $(".section .container .box").remove(); $("#sort-toggle-btn").remove(); this.$contentBox.append('<div class="tool-box" style="margin-top: 10px"></div>'); this.$contentBox.append('<div class="movie-list h cols-4 vcols-8" style="margin-top: 10px"></div>'); this.$contentBox.append('<div class="page-box"></div>'); $(".tool-box").append('\n <div class="button-group">\n <div class="buttons has-addons" id="conditionBox">\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="release_date">发布日期</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="recent_update">最近更新</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="trending">热门</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed_today">今天最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed_week">本周最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed_month">本月最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_viewed">最多观看</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-sort="most_favourited">最受欢迎</a>\n </div>\n </div>\n '); $(`#conditionBox a[data-sort="${this.sortVal}"]`).addClass("is-info"); utils.setHrefParam("sort", this.sortVal); utils.setHrefParam("page", this.currentPage); $("#conditionBox").on("click", "a.button", (e => { let $target = $(e.target); this.sortVal = $target.data("sort"); utils.setHrefParam("sort", this.sortVal); $target.siblings().removeClass("is-info"); $target.addClass("is-info"); this.handleQuery(); })); $(".page-box").append('\n <nav class="pagination">\n <a class="pagination-previous">上一页</a>\n <ul class="pagination-list"></ul>\n <a class="pagination-next">下一页</a>\n </nav>\n '); $(document).on("click", ".pagination-link", (e => { e.preventDefault(); this.currentPage = parseInt($(e.target).data("page")); utils.setHrefParam("page", this.currentPage); this.renderPagination(); this.handleQuery(); })); $(".pagination-previous").on("click", (e => { e.preventDefault(); if (this.currentPage > 1) { this.currentPage--; utils.setHrefParam("page", this.currentPage); this.renderPagination(); this.handleQuery(); } })); $(".pagination-next").on("click", (e => { e.preventDefault(); if (this.currentPage < this.maxPage) { this.currentPage++; utils.setHrefParam("page", this.currentPage); this.renderPagination(); this.handleQuery(); } })); } renderPagination() { const $paginationList = $(".pagination-list"); $paginationList.empty(); let startPage = Math.max(1, this.currentPage - 2), endPage = Math.min(this.maxPage, this.currentPage + 2); this.currentPage <= 3 ? endPage = Math.min(6, this.maxPage) : this.currentPage >= this.maxPage - 2 && (startPage = Math.max(this.maxPage - 5, 1)); if (startPage > 1) { $paginationList.append('<li><a class="pagination-link" data-page="1">1</a></li>'); startPage > 2 && $paginationList.append('<li><span class="pagination-ellipsis">…</span></li>'); } for (let i = startPage; i <= endPage; i++) { const activeClass = i === this.currentPage ? " is-current" : ""; $paginationList.append(`<li><a class="pagination-link${activeClass}" data-page="${i}">${i}</a></li>`); } if (endPage < this.maxPage) { endPage < this.maxPage - 1 && $paginationList.append('<li><span class="pagination-ellipsis">…</span></li>'); $paginationList.append(`<li><a class="pagination-link" data-page="${this.maxPage}">${this.maxPage}</a></li>`); } } async handleQuery() { let loadObj = loading(); try { let pagesToFetch = []; pagesToFetch = 1 === this.currentPage ? [ 1, 2 ] : [ 2 * this.currentPage - 1, 2 * this.currentPage ]; if (this.keyword) { pagesToFetch = [ 1 ]; $(".page-box").hide(); $(".tool-box").hide(); } const fetchPromises = pagesToFetch.map((page => { let url = `${this.baseUrl}/dm4/tags/fc2?sort=${this.sortVal}&page=${page}`; this.keyword && (url = `${this.baseUrl}/search?keyword=${this.keyword}`); console.log(url); return gmHttp.get(url); })), htmlResults = await Promise.all(fetchPromises); let dataList = []; for (const html of htmlResults) { console.log(html); let $dom = $(html); $dom.find(".box-item").each(((index, element) => { const $item = $(element), imgSrc = $item.find("img").attr("data-src"); let carNum2 = $item.find("img").attr("title"); const detailLink = $item.find(".detail a"), link = detailLink.attr("href"), href = this.baseUrl + (link.startsWith("/") ? link : "/" + link), title = detailLink.text().trim().replace(carNum2 + " - ", ""); carNum2 = carNum2.replace("FC2-PPV", "FC2"); dataList.push({ imgSrc: imgSrc, carNum: carNum2, href: href, title: title }); })); if (!this.maxPage) { let rawMaxPage, lastPageItem = $dom.find(".page-item:not(.disabled)").last(); if (lastPageItem.find("a.page-link").length) { let href = lastPageItem.find("a.page-link").attr("href"); rawMaxPage = parseInt(href.split("page=")[1]); } else rawMaxPage = parseInt(lastPageItem.find("span.page-link").text()); this.maxPage = Math.ceil(rawMaxPage / 2); this.renderPagination(); } } if (0 === dataList.length) { show.error("获取数据失败!"); return; } let movieHtml = this.markDataListHtml(dataList); $(".movie-list").html(movieHtml); await utils.smoothScrollToTop(); } catch (e) { console.error(e); } finally { loadObj.close(); } } open123AvFc2Dialog(carNum2, href) { layer.open({ type: 1, title: carNum2, 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="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\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 <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links">\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-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(carNum2, href); let keyword = carNum2.replace("FC2-", ""); $("#magnets-content").append(this.getBean("MagnetHubPlugin").createMagnetHub(keyword)); $("#favoriteBtn").on("click", (async event2 => { const actress = $("#data-actress").text(); await storageManager.saveCar(carNum2, href, actress, Status_FAVORITE); window.refresh(); layer.closeAll(); })); $("#filterBtn").on("click", (event2 => { utils.q(event2, `是否屏蔽${carNum2}?`, (async () => { const actress = $("#data-actress").text(); await storageManager.saveCar(carNum2, 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(carNum2, href, actress, Status_HAS_DOWN); window.refresh(); layer.closeAll(); })); $("#hasWatchBtn").on("click", (async event2 => { const actress = $("#data-actress").text(); await storageManager.saveCar(carNum2, href, actress, Status_HAS_WATCH); window.refresh(); layer.closeAll(); })); $("#search-subtitle-btn").on("click", (event2 => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event2))); $("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2))); } }); } async loadData(carNum2, href) { let loadObj = loading(); try { const {id: id, publishDate: publishDate, title: title, moviePoster: moviePoster} = await this.get123AvVideoInfo(href); $(".movie-info-container").html(`\n <h3 class="movie-title" style="margin-bottom: 10px">${title || "无标题"}</h3>\n <div class="movie-meta" style="margin-bottom: 10px">\n <span>番号: ${carNum2 || "未知"}</span>\n <span>年份: ${publishDate || "未知"}</span>\n <span>\n 站点: \n <a href="https://fc2ppvdb.com/articles/${carNum2.replace("FC2-", "")}" target="_blank">fc2ppvdb</a>\n <a style="margin-left: 5px;" href="https://adult.contents.fc2.com/article/${carNum2.replace("FC2-", "")}/" target="_blank">fc2电子市场</a>\n </span>\n </div>\n <div class="movie-actors" style="margin-bottom: 10px">\n <div class="actor-list">主演: </div>\n </div>\n <div class="movie-seller" style="margin-bottom: 10px">\n <span>販売者: </span>\n </div>\n <div class="movie-gallery" style="margin-bottom: 10px">\n <h4>剧照: </h4>\n <div class="image-list"></div>\n </div>\n `); this.getMovie(id, moviePoster).then((movieUrl => { $(".movie-trailer").attr("src", movieUrl); })); this.getImgList(carNum2).then(); this.getActressInfo(carNum2).then(); } catch (e) { console.error(e); } finally { loadObj.close(); } } async get123AvVideoInfo(href) { const html = await gmHttp.get(href), match = html.match(/v-scope="Movie\({id:\s*(\d+),/), id = match ? match[1] : null, $dom = $(html); return { id: id, publishDate: $dom.find('span:contains("リリース日:")').next("span").text(), title: $dom.find("h1").text().trim(), moviePoster: $dom.find("#player").attr("data-poster") }; } async getActressInfo(fc2Num) { let url = `https://fc2ppvdb.com/articles/${fc2Num.replace("FC2-", "")}`; const html = await gmHttp.get(url), $dom = $(html), actressNodeList = $dom.find("div").filter((function() { return 0 === $(this).text().trim().indexOf("女優:"); })); if (0 === actressNodeList.length || actressNodeList.length > 1) { show.error("解析女优信息失败"); return; } const $actress = $(actressNodeList[0]).find("a"); let actorsHtml = "主演: "; if ($actress.length > 0) { let actress = ""; $actress.each(((index, ele) => { let $actor = $(ele), name = $actor.text(), actressHref = $actor.attr("href"); actorsHtml += `<span class="actor-tag"><a href="https://fc2ppvdb.com${actressHref}" target="_blank">${name}</a></span>`; actress += name + " "; })); $("#data-actress").text(actress); } else actorsHtml += "<span>暂无演员信息</span>"; $(".actor-list").html(actorsHtml); const sellerNodeList = $dom.find("div").filter((function() { return 0 === $(this).text().trim().indexOf("販売者:"); })); if (sellerNodeList.length > 0) { const $sellerA = $(sellerNodeList[0]).find("a"); if ($sellerA.length > 0) { const $seller = $($sellerA[0]); console.log($seller); let name = $seller.text(), sellerHref = $seller.attr("href"); $(".movie-seller").html(`<span> 販売者: <a href="https://fc2ppvdb.com${sellerHref}" target="_blank">${name}</a></span>`); } } } async getImgList(fc2Num) { let url = `https://adult.contents.fc2.com/article/${fc2Num.replace("FC2-", "")}/`; const html = await gmHttp.get(url, null, { referer: url }); let imgList = $(html).find(".items_article_SampleImagesArea img").map((function() { return $(this).attr("src"); })).get(), imagesHtml = ""; Array.isArray(imgList) && imgList.length > 0 ? imagesHtml = 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("") : $(".movie-gallery").html("<h4>剧照: 暂无剧照</h4>"); $(".image-list").html(imagesHtml); } async getMovie(id, moviePoster) { let url = `${this.baseUrl}/ajax/v/${id}/videos`, loadObj = loading(); try { let movieList = (await gmHttp.get(url)).result.watch; return movieList.length > 0 ? movieList[0].url + "?poster=" + moviePoster : null; } catch (e) { console.error(e); } finally { loadObj.close(); } } markDataListHtml(movies) { let moviesHtml = ""; movies.forEach((movie => { moviesHtml += `\n <div class="item">\n <a href="${movie.href}" class="box" title="${movie.title}">\n <div class="cover ">\n <img loading="lazy" src="${movie.imgSrc.replace("/s360", "")}" alt="">\n </div>\n <div class="video-title"><strong>${movie.carNum}</strong> ${movie.title}</div>\n <div class="score">\n </div>\n <div class="meta">\n </div>\n <div class="tags has-addons">\n </div>\n </a>\n </div>\n `; })); return moviesHtml; } } class video123AvPlugin extends BasePlugin { async handle() { if (currentHref.includes("5masterzzz")) { localStorage.setItem("__pul", Date.now().toString()); setInterval((() => { localStorage.setItem("__pul", Date.now().toString()); }), 5e3); } } } class MagnetHubPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "currentEngine", null); __publicField(this, "searchEngines", [ { name: "U3C3", id: "u3c3", url: "https://u3c3.com/?search2=eelj1a3lfe1a1&search={keyword}", parse: this.parseU3C3 }, { name: "BTSOW", id: "BTSOW", url: "https://btsow.pics/search/{keyword}", parse: this.parseBTSOW } ]); } async initCss() { return "\n <style>\n .magnet-container {\n margin: 20px auto;\n width: 100%;\n font-family: Arial, sans-serif;\n }\n .magnet-tabs {\n display: flex;\n border-bottom: 1px solid #ddd;\n margin-bottom: 15px;\n }\n .magnet-tab {\n padding: 5px 12px;\n cursor: pointer;\n border: 1px solid transparent;\n border-bottom: none;\n margin-right: 5px;\n background: #f5f5f5;\n border-radius: 5px 5px 0 0;\n }\n .magnet-tab.active {\n background: #fff;\n border-color: #ddd;\n border-bottom: 1px solid #fff;\n margin-bottom: -1px;\n font-weight: bold;\n }\n .magnet-tab:hover:not(.active) {\n background: #e9e9e9;\n }\n \n .magnet-results {\n min-height: 200px;\n }\n .magnet-result {\n padding: 15px;\n border-bottom: 1px solid #eee;\n position: relative; \n }\n .magnet-result:hover {\n background-color: #f9f9f9;\n }\n .magnet-title {\n font-weight: bold;\n margin-bottom: 5px;\n white-space: nowrap;\n overflow: hidden; \n text-overflow: ellipsis;\n padding-right: 80px; \n }\n .magnet-info {\n display: flex;\n justify-content: space-between;\n font-size: 12px;\n color: #666;\n margin-bottom: 5px;\n }\n .magnet-loading {\n text-align: center;\n padding: 20px;\n }\n .magnet-error {\n color: #f44336;\n padding: 10px;\n }\n \n .magnet-copy {\n position: absolute;\n right: 15px;\n top: 12px;\n }\n .copy-btn {\n background-color: #f0f0f0;\n color: #555;\n border: 1px solid #ddd;\n padding: 3px 8px;\n border-radius: 3px;\n cursor: pointer;\n font-size: 12px;\n transition: all 0.2s;\n }\n .copy-btn:hover {\n background-color: #e0e0e0;\n border-color: #ccc;\n }\n .copy-btn.copied {\n background-color: #4CAF50;\n color: white;\n border-color: #4CAF50;\n }\n </style>\n "; } createMagnetHub(keyword) { const $container = $('<div class="magnet-container"></div>'), $tabs = $('<div class="magnet-tabs"></div>'), savedEngineId = localStorage.getItem("magnetHub_selectedEngine"); let defaultEngineIndex = 0; this.searchEngines.forEach(((engine, index) => { const $tab = $(`<div class="magnet-tab" data-engine="${engine.id}">${engine.name}</div>`); if (savedEngineId && engine.id === savedEngineId) { $tab.addClass("active"); this.currentEngine = engine; defaultEngineIndex = index; } else if (0 === index && !savedEngineId) { $tab.addClass("active"); this.currentEngine = engine; } $tabs.append($tab); })); $container.append($tabs); const $resultsContainer = $('<div class="magnet-results"></div>'); $container.append($resultsContainer); $container.on("click", ".magnet-tab", (e => { const engineId = $(e.target).data("engine"); this.currentEngine = this.searchEngines.find((engine => engine.id === engineId)); localStorage.setItem("magnetHub_selectedEngine", engineId); $container.find(".magnet-tab").removeClass("active"); $(e.target).addClass("active"); this.searchEngine($resultsContainer, this.currentEngine, keyword); })); this.searchEngine($resultsContainer, this.currentEngine || this.searchEngines[defaultEngineIndex], keyword); return $container; } searchEngine($container, engine, keyword) { $container.html(`<div class="magnet-loading">正在从 ${engine.name} 搜索 "${keyword}"...</div>`); const cacheKey = `${engine.name}_${keyword}`, cachedResult = sessionStorage.getItem(cacheKey); if (cachedResult) try { const results = JSON.parse(cachedResult); this.displayResults($container, results, engine.name); return; } catch (e) { $container.html(`<div class="magnet-error">解析 ${engine.name} 缓存结果失败: ${e.message}</div>`); } const url = engine.url.replace("{keyword}", encodeURIComponent(keyword)); GM_xmlhttpRequest({ method: "GET", url: url, onload: response => { try { const results = engine.parse.call(this, response.responseText); results.length > 0 && sessionStorage.setItem(cacheKey, JSON.stringify(results)); this.displayResults($container, results, engine.name); } catch (e) { $container.html(`<div class="magnet-error">解析 ${engine.name} 结果失败: ${e.message}</div>`); } }, onerror: error => { $container.html(`<div class="magnet-error">从 ${engine.name} 获取数据失败: ${error.statusText}</div>`); } }); } displayResults($container, results, engineName) { $container.empty(); if (0 !== results.length) { results.forEach((result => { const $result = $(`\n <div class="magnet-result">\n <div class="magnet-title"><a href="${result.magnet}">${result.title}</a></div>\n <div class="magnet-info">\n <span>大小: ${result.size || "未知"}</span>\n <span>日期: ${result.date || "未知"}</span>\n </div>\n <div class="magnet-copy">\n <button class="copy-btn" data-magnet="${result.magnet}">复制链接</button>\n </div>\n </div>\n `); $container.append($result); })); $container.on("click", ".copy-btn", (function() { const $btn = $(this), magnet = $btn.data("magnet"); navigator.clipboard ? navigator.clipboard.writeText(magnet).then((() => { showCopiedFeedback($btn); })).catch((err => { fallbackCopy(magnet, $btn); })) : fallbackCopy(magnet, $btn); })); } else $container.append('<div class="magnet-error">没有找到相关结果</div>'); function showCopiedFeedback($btn) { const originalText = $btn.text(); $btn.addClass("copied").text("已复制"); setTimeout((() => { $btn.removeClass("copied").text(originalText); }), 2e3); } function fallbackCopy(text, $btn) { const textarea = document.createElement("textarea"); textarea.value = text; textarea.style.position = "fixed"; document.body.appendChild(textarea); textarea.select(); try { document.execCommand("copy"); showCopiedFeedback($btn); } catch (err) { console.error("复制失败:", err); alert("复制失败,请手动复制链接"); } document.body.removeChild(textarea); } } parseBTSOW(html) { const $dom = $(html), results = []; $dom.find(".data-list .row").each(((i, el) => { const $el = $(el); let $a = $el.find("a"); if (0 === $a.length) return; const title = $a.attr("title"), magnet = "magnet:?xt=urn:btih:" + $a.attr("href").split("/").pop(), size = $el.find(".size").text(), date = $el.find(".date").text(); results.push({ title: title, magnet: magnet, size: size, date: date }); })); return results; } parseU3C3(html) { const $dom = $(html), results = []; $dom.find(".torrent-list tbody tr").each(((i, el) => { const $el = $(el); if ($el.text().includes("置顶")) return; const title = $el.find("td:nth-child(2) a").attr("title") || $el.find("td:nth-child(2) a").text().trim(), magnet = $el.find("td:nth-child(3) a[href^='magnet:']").attr("href"), size = $el.find("td:nth-child(4)").text().trim(), date = $el.find("td:nth-child(5)").text().trim(); magnet && results.push({ title: title, magnet: magnet, size: size, date: date }); })); return results; } } class ScreenShotPlugin extends BasePlugin { async handle() { if (!isDetailPage) return; let carNum2 = this.getPageInfo().carNum; Promise.any([ this.img_3xplanet(carNum2), this.img_memojav(carNum2) ]).then((imgUrl => { this.addImg("缩略图", imgUrl); })).catch((e => { console.error(e); })); } addImg(title, imgUrl) { if (!imgUrl) return; let simpleId = utils.simpleId(); isJavDb && $(".preview-images").append(`\n <a class="tile-item" data-fancybox="gallery" data-caption="${title}" data-src="#${simpleId}">\n <img src="${imgUrl}" alt="${title}" loading="lazy" style="max-height: 100px">\n </a> \n <div id="${simpleId}" style="display: none;"><img src="${imgUrl}" alt="${title}" loading="lazy" style="width: 85vw;"></div>\n `); if (isJavBus) { $("#sample-waterfall .sample-box:last").after(`\n <a class="sample-box" style="height: 110px; overflow:hidden;" id="${simpleId}" href="${imgUrl}"><div class="photo-frame"><img src="${imgUrl}" style="height: inherit;width: 100%;" title="${title}" alt="${title}"></div></a>\n `); $(`#${simpleId}`).on("click", (event2 => { event2.preventDefault(); event2.stopPropagation(); layer.photos({ photos: { title: title, start: 0, data: [ { alt: title, pid: 0, src: imgUrl } ] }, footer: !1, success: function(layero) { layero.find('[toolbar-event="zoom"][data-option="0.1"]').attr("data-option", .5); layero.find('[toolbar-event="zoom"][data-option="-0.1"]').attr("data-option", -.5); } }); })); } } async img_3xplanet(carNum2) { let url = `https://3xplanet.com/?s=${carNum2}`, html = await gmHttp.get(url); const href = $(html).find(".td-image-wrap").first().attr("href"); if (!href) { console.error("解析3xplanet搜索页失败:", url); return; } const detailPageHtml = await gmHttp.get(href); let imgUrl = $(detailPageHtml).find('img[src*="s200"]').attr("src"); if (imgUrl) { imgUrl = imgUrl.replace("s200", "s0"); return imgUrl; } console.error("解析3xplanet缩略图失败:", url); } async img_memojav(carNum2) { let url = `https://memojav.com/image/screenshot/${carNum2}.jpg`; try { await gmHttp.gmRequest("HEAD", url); return url; } catch (e) { console.error("memojav缩略图不可用:", e); return null; } } } 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.split("?")[0].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("advanced_search") : !!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(Fc2By123AvPlugin); 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); pluginManager.register(MagnetHubPlugin); pluginManager.register(ScreenShotPlugin); } 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); pluginManager.register(MagnetHubPlugin); pluginManager.register(ScreenShotPlugin); } hostname.includes("sehuatang") && pluginManager.register(SeHuaTangPlugin); hostname.includes("javtrailers") && pluginManager.register(JavTrailersPlugin); hostname.includes("subtitlecat") && pluginManager.register(SubTitleCatPlugin); hostname.includes("aliyundrive") && pluginManager.register(AliyunPanPlugin); hostname.includes("5masterzzz") && pluginManager.register(video123AvPlugin); pluginManager.process().then(); }(); }; }();