// ==UserScript== // @name JAV-JHS // @namespace JAV-JHS // @version 3.3.6-fix // @author xie bro // @description Jav-鉴黄师 收藏、屏蔽、标记已下载; 屏蔽标签、屏蔽演员、同步收藏演员、新作品检测; 免VIP查看热播、Top250排行榜、Fc2ppv、可查看所有评论信息、相关清单; 支持云盘备份; 以图识图; 字幕搜索; JavDb|JavBus // @license MIT // @icon https://www.google.com/s2/favicons?sz=64&domain=javdb.com // @match https://javdb.com/* // @match https://www.javbus.com/* // @include https://javdb*.com/* // @include https://*javbus*/* // @include https://*javsee*/* // @include https://*seejav*/* // @include https://javtrailers.com/* // @include https://subtitlecat.com/* // @include https://www.aliyundrive.com/* // @include https://www.alipan.com/* // @include https://115.com/* // @exclude https://*javbus*/forum/* // @exclude https://*javbus*/*actresses // @exclude https://*javsee*/forum/* // @exclude https://*javsee*/*actresses // @exclude https://*seejav*/forum/* // @exclude https://*seejav*/*actresses // @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js // @require https://cdn.jsdelivr.net/npm/tabulator-tables@6.3.1/dist/js/tabulator.min.js // @require https://cdn.jsdelivr.net/npm/layui-layer@1.0.9/dist/layer.min.js // @require https://cdn.jsdelivr.net/npm/blueimp-md5@2.19.0/js/md5.min.js // @require https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js // @require https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js // @require https://cdn.jsdelivr.net/npm/viewerjs@1.11.1/dist/viewer.min.js // @require https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.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 www.dmm.com // @connect api.dmm.com // @connect special.dmm.co.jp // @connect adult.contents.fc2.com // @connect fc2ppvdb.com // @connect 123av.com // @connect u3c3.com // @connect u9a9.com // @connect btsow.lol // @connect sukebei.nyaa.si // @connect javstore.net // @connect javbest.net // @connect missav.live // @connect jable.tv // @connect www.av.gl // @connect jav.rs // @connect javtrailers.com // @connect javdb.com // @connect javbus.com // @connect supjav.com // @connect 115.com // @connect jianguoyun.com // @connect 127.0.0.1 // @connect * // @grant GM_xmlhttpRequest // @grant GM_openInTab // @grant unsafeWindow // @run-at document-idle // ==/UserScript== var _a, _StorageManager_instances, setItem_fn, saveFilterItem_fn, __defProp = Object.defineProperty, __typeError = msg => { throw TypeError(msg); }, __publicField = (obj, key, value) => ((obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: !0, configurable: !0, writable: !0, value: value }) : obj[key] = value)(obj, "symbol" != typeof key ? key + "" : key, value), __privateMethod = (obj, member, method) => (((obj, member, msg) => { member.has(obj) || __typeError("Cannot " + msg); })(obj, member, "access private method"), method); const currentHref = window.location.href, isJavDb = currentHref.includes("javdb"), isJavBus = currentHref.includes("javbus") || currentHref.includes("seejav") || currentHref.includes("bus") || currentHref.includes("javsee") || "javbus" === (null == (_a = $(".hidden-xs").attr("alt")) ? void 0 : _a.trim().toLowerCase()), 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", NO = "no", YES = "yes", qualityOptions = [ { id: "video-mhb", quality: "dmb_w", text: "旧视频源-中画质宽版 (404p)", canSelect: !1 }, { id: "video-mhb", quality: "sm_s", text: "旧视频源-低画质 (240p)", canSelect: !1 }, { id: "video-mhb", quality: "dm_s", text: "旧视频源-中画质 (360p)", canSelect: !1 }, { id: "video-mhb", quality: "dmb_s", text: "旧视频源-中画质 (480p)", canSelect: !1 }, { id: "video-mhb", quality: "mhb_w", text: "旧视频源-高画质宽版 (404p)", canSelect: !1 }, { id: "video-mmb", quality: "mmb", text: "中画质 (432p)", canSelect: !0 }, { id: "video-mhb", quality: "mhb", text: "高画质 (576p)", canSelect: !0 }, { id: "video-hmb", quality: "hmb", text: "HD (720p)", canSelect: !0 }, { id: "video-hhb", quality: "hhb", text: "FullHD (1080p)", canSelect: !0 }, { id: "video-hhbs", quality: "hhbs", text: "FullHD (1080p60fps)", canSelect: !0 }, { id: "video-4k", quality: "4k", text: "4K (2160p)", canSelect: !0 }, { id: "video-4ks", quality: "4ks", text: "4K (2160p60fps)", canSelect: !0 } ]; let detailPageCss$1 = ""; window.location.href.includes("hideNav=1") && (detailPageCss$1 = "\n .navbar-default {\n display: none !important;\n }\n body {\n padding-top:0px!important;\n }\n "); const javBusStyle = `\n\n`; let detailPageCss = ""; window.location.href.includes("hideNav=1") && (detailPageCss = "\n .main-nav,#search-bar-container {\n display: none !important;\n }\n \n html {\n padding-top:0px!important;\n }\n "); const javdbStyle = `\n\n`; const mainCss = `\n\n`; function insertStyle(css) { if (css) if (css.includes("\n"); insertStyle(mainCss); _StorageManager_instances = new WeakSet; setItem_fn = async function(key, data) { key === this.favorite_actresses_key && window.clean_cacheFavoriteActressList(); key === this.blacklist_car_list_key && window.clean_cacheBlacklistCarList(); key === this.setting_key && window.clean_cacheSettingObj(); key === this.car_list_key && window.clean_cacheCarList(); await this.forage.setItem(key, data); }; 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 __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, storageKey, itemList); return itemList; }; let StorageManager = class _StorageManager { constructor() { obj = this, (member = _StorageManager_instances).has(obj) ? __typeError("Cannot add the same private member more than once") : member instanceof WeakSet ? member.add(obj) : member.set(obj, value); var obj, member, value; __publicField(this, "car_list_key", "car_list"); __publicField(this, "filter_keyword_title_key", "filter_keyword_title"); __publicField(this, "filter_keyword_review_key", "filter_keyword_review"); __publicField(this, "setting_key", "setting"); __publicField(this, "blacklist_key", "blacklist"); __publicField(this, "blacklist_car_list_key", "blacklist_car_list"); __publicField(this, "favorite_actresses_key", "favorite_actresses"); __publicField(this, "highlighted_tags_key", "highlighted_tags"); __publicField(this, "forage", localforage.createInstance({ driver: localforage.INDEXEDDB, name: "JAV-JHS", version: 1, storeName: "appData" })); __publicField(this, "cacheCarList", null); __publicField(this, "cacheBlacklistCarList", null); __publicField(this, "cacheFavoriteActressList", null); __publicField(this, "cacheSettingObj", null); if (_StorageManager.instance) throw new Error("StorageManager已被实例化过了!"); _StorageManager.instance = this; } async getCarList() { if (this.cacheCarList) return utils.copyObj(this.cacheCarList); this.cacheCarList = await this.forage.getItem(this.car_list_key) || []; return utils.copyObj(this.cacheCarList); } async getCar(carNum2) { return (await this.getCarList()).find((item => item.carNum === carNum2)); } _handleSingleCar(carParam, carList) { let {carNum: carNum2, url: url, names: names, actionType: actionType, publishTime: publishTime, starId: starId} = carParam; if (!carNum2) { show.error("番号为空!"); throw new Error("番号为空!"); } if (!url) { show.error("url为空!"); throw new Error("url为空!"); } url.includes("http") || (url = window.location.origin + url); names && (names = names.trim()); let carData = carList.find((item => item.carNum === carNum2)); if (carData) { names && (carData.names = names); url && (carData.url = url); publishTime && (carData.publishTime = publishTime); carData.updateDate = utils.getNowStr(); } else { let nowStr = utils.getNowStr(); carData = { carNum: carNum2, url: url, names: names, status: "", createDate: nowStr, updateDate: nowStr, publishTime: publishTime }; starId && (carData.starId = starId); carList.push(carData); } 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错误, 请联系作者更正: " + actionType; show.error(msg); throw new Error(msg); } } async saveCar(carData) { const carList = await this.forage.getItem(this.car_list_key) || []; this._handleSingleCar(carData, carList); await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.car_list_key, carList); await this.removeNewVideoList([ carData.carNum ]); } async updateCarInfo(carParam) { let {carNum: carNum2, url: url, names: names, actionType: actionType, publishTime: publishTime, remark: remark} = carParam; if (!carNum2) { show.error("番号为空!"); throw new Error("番号为空!"); } if (!url) { show.error("url为空!"); throw new Error("url为空!"); } names && (names = names.trim()); const carList = await this.forage.getItem(this.car_list_key) || []; let carData = carList.find((item => item.carNum === carNum2)); if (!carData) { const msg = "数据不存在: " + carNum2; show.error(msg); throw new Error(msg); } carData.names = names; carData.url = url; carData.remark = remark; carData.updateDate = utils.getNowStr(); switch (actionType) { case Status_FILTER: carData.status = Status_FILTER; break; case Status_FAVORITE: 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错误, 请联系作者更正: " + actionType; show.error(msg); throw new Error(msg); } await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.car_list_key, carList); } async saveCarList(carRecords) { if (!carRecords || !Array.isArray(carRecords) || 0 === carRecords.length) { show.error("记录列表为空!"); throw new Error("记录列表为空!"); } const carList = await this.forage.getItem(this.car_list_key) || []; for (const record of carRecords) try { this._handleSingleCar(record, carList); } catch (error) { throw error; } await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.car_list_key, carList); } async removeNewVideoList(carNumList) { const favoriteActressesList = await this.getFavoriteActressList(); let hasAnyListBeenModified = !1; const updatedActressList = favoriteActressesList.map((actress => { if (!actress.newVideoList || !Array.isArray(actress.newVideoList)) return actress; const newFilteredVideoList = actress.newVideoList.filter((carNumString => { const isRemoved = carNumList.includes(carNumString); if (isRemoved) { clog.log("移除关联女优新作品", actress.name, carNumString); hasAnyListBeenModified = !0; } return !isRemoved; })); return { ...actress, newVideoList: newFilteredVideoList }; })); hasAnyListBeenModified && await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.favorite_actresses_key, updatedActressList); } 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 __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.car_list_key, updatedList); return !0; } async batchRemoveCars(carNumList) { if (!carNumList || 0 === carNumList.length) throw new Error("未传入参数"); const carList = await this.getCarList(), initialLength = carList.length, carNumsSet = new Set(carNumList), updatedList = carList.filter((car => !carNumsSet.has(car.carNum))), removedCount = initialLength - updatedList.length; if (0 === removedCount) return removedCount; await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.car_list_key, updatedList); return removedCount; } async getBlacklist() { return await this.forage.getItem(this.blacklist_key) || []; } async addBlacklistItem(item) { let {starId: starId, name: name2, allName: allName, role: role, movieType: movieType, url: url} = item; if (!starId) throw new Error("缺失starId"); if (!name2) throw new Error("缺失name"); if (!role) throw new Error("缺失role"); const blacklist = await this.getBlacklist(), existData = blacklist.find((item2 => item2.starId === starId)); if (existData) { existData.url = url; existData.role = role; existData.movieType = movieType; clog.log("更新黑名单演员信息", existData); } else { const info = { starId: starId, name: name2, allName: allName || [ name2 ], createTime: utils.getNowStr(), role: role, movieType: movieType, url: url }; blacklist.push(info); clog.log("增加黑名单演员信息", info); } await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.blacklist_key, blacklist); } async updateBlacklistItem(item) { if (!item || !item.starId) throw new Error("参数不全"); const filterActorActressInfoList = await this.getBlacklist(), waitUpdateObj = filterActorActressInfoList.find((i => i.starId === item.starId)); if (!waitUpdateObj) throw new Error(`未找到黑名单演员信息:${item.name} ${item.starId}`); item.checkTime && (waitUpdateObj.checkTime = item.checkTime); item.lastPublishTime && (waitUpdateObj.lastPublishTime = item.lastPublishTime); await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.blacklist_key, filterActorActressInfoList); } async deleteBlacklistItem(starId) { const blacklist = await this.getBlacklist(), updatedList = blacklist.filter((item => item.starId !== starId)); blacklist.length !== updatedList.length && await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.blacklist_key, updatedList); } async getBlacklistCarList() { if (this.cacheBlacklistCarList && this.cacheBlacklistCarList.length > 0) return utils.deepFreeze(this.cacheBlacklistCarList); this.cacheBlacklistCarList = await this.forage.getItem(this.blacklist_car_list_key) || []; setTimeout((() => { utils.deepFreeze(this.cacheBlacklistCarList); }), 0); return this.cacheBlacklistCarList; } async batchSaveBlacklistCarList(carDataList) { const carList = await this.getCarList(); let blacklistCarList = await this.getBlacklistCarList(), hasChanged = !1, copyBlacklistCarList = null, carNumList = []; for (const carData of carDataList) { let existBlacklistCarData = blacklistCarList.find((item => item.carNum === carData.carNum)); if (existBlacklistCarData) { console.log("已在鉴定记录中", existBlacklistCarData); continue; } let existCarData = carList.find((item => item.carNum === carData.carNum)); if (existCarData) console.log("已在鉴定记录中", existCarData); else { copyBlacklistCarList || (copyBlacklistCarList = utils.copyObj(blacklistCarList)); this._handleSingleCar(carData, copyBlacklistCarList); clog.log(`屏蔽演员番号: ${carData.names} ${carData.carNum}`); hasChanged = !0; carNumList.push(carData.carNum); } } if (hasChanged) { if (!copyBlacklistCarList) throw new Error("程序异常, 黑名单番号数据对象为空!"); await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.blacklist_car_list_key, copyBlacklistCarList); await this.removeNewVideoList(carNumList); } } async removeBlacklistCarList(starId) { const filterActorActressCarList = await this.getBlacklistCarList(), newList = filterActorActressCarList.filter((car => car.starId !== starId)); newList.length !== filterActorActressCarList.length && await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.blacklist_car_list_key, newList); } async batchRemoveBlacklistCars(carNumList) { if (!carNumList || 0 === carNumList.length) throw new Error("未传入参数"); const blacklistCarList = await this.getBlacklistCarList(), initialLength = blacklistCarList.length, carNumsSet = new Set(carNumList), updatedList = blacklistCarList.filter((car => !carNumsSet.has(car.carNum))), removedCount = initialLength - updatedList.length; if (0 === removedCount) return removedCount; await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.blacklist_car_list_key, updatedList); return removedCount; } async getFavoriteActressList() { if (this.cacheFavoriteActressList) return utils.copyObj(this.cacheFavoriteActressList); this.cacheFavoriteActressList = await this.forage.getItem(this.favorite_actresses_key) || []; return utils.copyObj(this.cacheFavoriteActressList); } async addFavoriteActressList(actressList) { const favoriteActressesInfoList = await this.getFavoriteActressList(); let resultCount = 0; for (const actress of actressList) { let {starId: starId, name: name2, allName: allName, avatar: avatar, lastCheckTime: lastCheckTime, lastPublishTime: lastPublishTime, actressType: actressType} = actress; if (!starId) throw new Error("缺失starId"); if (!name2) throw new Error("缺失name"); allName || (allName = [ name2 ]); const uncensoredText = "(無碼)"; if (!actressType) { actressType = name2.includes(uncensoredText) || allName.some((element => element.includes(uncensoredText))) ? "uncensored" : "censored"; } name2 = name2.replace(uncensoredText, ""); allName = allName.map((n => n.replace(uncensoredText, ""))); let favoriteActresses = favoriteActressesInfoList.find((item => item.starId === starId)); if (favoriteActresses) { if ((!favoriteActresses.avatar || !favoriteActresses.avatar.includes("https")) && avatar) { clog.log(avatar); favoriteActresses.avatar = avatar; clog.log(`补全女优头像: ${name2}`); resultCount++; } if (!favoriteActresses.actressType && actressType) { favoriteActresses.actressType = actressType; clog.log(`补全女优类别: ${name2} ${actressType}`); resultCount++; } if (favoriteActresses.name.includes(uncensoredText)) { favoriteActresses.name = name2; favoriteActresses.allName = allName; clog.log(`更正女优名字: ${name2} ${allName}`); resultCount++; } continue; } const nowStr = utils.getNowStr(); favoriteActressesInfoList.push({ starId: starId, name: name2, allName: allName, avatar: avatar, lastCheckTime: lastCheckTime, lastPublishTime: lastPublishTime, createDate: nowStr, updateDate: nowStr, actressType: actressType }); clog.log(`同步JavDB已收藏的演员: ${name2}`); resultCount++; } resultCount > 0 ? await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.favorite_actresses_key, favoriteActressesInfoList) : clog.log("信息已记录, 无需要进行同步收藏的演员"); return resultCount; } async removeFavoriteActress(starId) { const favoriteActressesInfoList = await this.getFavoriteActressList(), initialLength = favoriteActressesInfoList.length, updatedList = favoriteActressesInfoList.filter((car => car.starId !== starId)); if (updatedList.length === initialLength) { clog.error(`移除演员失败, ${starId} 不存在`); return !1; } await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.favorite_actresses_key, updatedList); return !0; } async updateFavoriteActress(actress) { const favoriteActressesInfoList = await this.getFavoriteActressList(), {starId: starId, name: name2, allName: allName, avatar: avatar, lastCheckTime: lastCheckTime, newVideoList: newVideoList, lastPublishTime: lastPublishTime, actressType: actressType, remark: remark} = actress; if (!starId) throw new Error("缺失starId"); let favoriteActresses = favoriteActressesInfoList.find((item => item.starId === starId)); if (!favoriteActresses) { clog.error("未找到演员信息", starId, name2); return !1; } name2 && (favoriteActresses.name = name2); allName && (favoriteActresses.allName = allName); avatar && (favoriteActresses.avatar = avatar); null != actressType && (favoriteActresses.actressType = actressType); lastCheckTime && (favoriteActresses.lastCheckTime = lastCheckTime); newVideoList && (favoriteActresses.newVideoList = newVideoList); lastPublishTime && (favoriteActresses.lastPublishTime = lastPublishTime); remark && (favoriteActresses.remark = remark); favoriteActresses.updateDate = utils.getNowStr(); await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.favorite_actresses_key, favoriteActressesInfoList); } async getHighlightedTags() { return await this.forage.getItem(this.highlighted_tags_key) || []; } async setHighlightedTags(highlightedTags) { return await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.highlighted_tags_key, highlightedTags); } async saveTitleFilterKeyword(keywords) { await __privateMethod(this, _StorageManager_instances, saveFilterItem_fn).call(this, keywords, this.filter_keyword_title_key, "标题关键词"); if (Array.isArray(keywords)) return null; const favoriteActressesList = await this.getFavoriteActressList(); let hasAnyListBeenModified = !1; const updatedActressList = favoriteActressesList.map((actress => { if (!actress.newVideoList || !Array.isArray(actress.newVideoList)) return actress; const newFilteredVideoList = actress.newVideoList.filter((carNumString => { const isRemoved = carNumString.startsWith(keywords); if (isRemoved) { clog.log("移除关联女优新作品", actress.name, carNumString); hasAnyListBeenModified = !0; } return !isRemoved; })); return { ...actress, newVideoList: newFilteredVideoList }; })); hasAnyListBeenModified && await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, this.favorite_actresses_key, updatedActressList); } async getTitleFilterKeyword() { return await this.forage.getItem(this.filter_keyword_title_key) || []; } async getReviewFilterKeywordList() { return await this.forage.getItem(this.filter_keyword_review_key) || []; } async saveReviewFilterKeyword(keywords) { return __privateMethod(this, _StorageManager_instances, saveFilterItem_fn).call(this, keywords, this.filter_keyword_review_key, "评论关键词"); } async getSetting(attribute = null, defaultVal) { this.cacheSettingObj || (this.cacheSettingObj = await this.forage.getItem(this.setting_key) || {}); let settingObj = utils.copyObj(this.cacheSettingObj); if (null === attribute) return settingObj; const value = settingObj[attribute]; return value ? "true" === value || "false" === value ? "true" === value.toLowerCase() : "string" != typeof value || "" === value.trim() || isNaN(Number(value)) ? value : Number(value) : defaultVal; } async saveSetting(settingObj) { settingObj ? await __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, 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 importData(jsonData) { await this.forage.clear(); const importPromises = []; for (const key in jsonData) { const value = jsonData[key], setPromise = __privateMethod(this, _StorageManager_instances, setItem_fn).call(this, key, value); importPromises.push(setPromise); } await Promise.all(importPromises); } async exportData() { const dataMap = {}; await this.forage.iterate(((value, key) => { dataMap[key] = value; })); if (0 === Object.keys(dataMap).length) throw new Error("没有可导出的数据"); return dataMap; } }; const apiUrl = "https://jdforrepam.com/api"; function buildSignature() { const curr = Math.floor(Date.now() / 1e3), stored_sign = localStorage.getItem("jhs_jdsignature"); if (stored_sign) { const parts = stored_sign.split("."); if (3 === parts.length) { if (curr - parseInt(parts[0]) <= 300) return stored_sign; } } const sign = `${curr}.lpw6vgqzsp.${md5(`${curr}71cf27bb3c0bcdf207b64abecddc970098c7421ee7203b9cdae54478478a199e7d5a6e1a57691123c1a931c057842fb73ba3b3c83bcd69c17ccf174081e3d8aa`)}`; localStorage.setItem("jhs_jdsignature", sign); return sign; } const javDbApi = { getReviews: async (movieId, pageNum = 1, pageSize = 20) => { let url = `${apiUrl}/v1/movies/${movieId}/reviews`, headers = { jdSignature: await buildSignature() }; return (await gmHttp.get(url, { page: pageNum, sort_by: "hotly", limit: pageSize }, headers)).data.reviews; }, searchMovie: 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; }, getMovieDetail: async movieId => { let url = `${apiUrl}/v4/movies/${movieId}`, headers = { jdSignature: await buildSignature() }; const res = await gmHttp.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.spfcas.com/rhe951l4q", "https://c0.jdbstatic.com")); })); return { movieId: movie.id, actors: movie.actors, duration: movie.duration, 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; }, getMagnets: async movieId => { let url = `${apiUrl}/v1/movies/${movieId}/magnets`, headers = { jdSignature: await buildSignature() }; return (await gmHttp.get(url, null, headers)).data.magnets; }, playback: 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 gmHttp.get(url, null, headers)).data.movies; }, login: async (username, password) => { let url = `${apiUrl}/v1/sessions?username=${encodeURIComponent(username)}&password=${encodeURIComponent(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); }, top250: 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 " + localStorage.getItem("jhs_appAuthorization"), jdsignature: await buildSignature() }; return await gmHttp.get(url, null, headers); }, buildSignature: buildSignature, markDataListHtml: movies => { let moviesHtml = ""; movies.forEach((movie => { moviesHtml += `\n
\n \n
\n \n
\n
${movie.number} ${movie.origin_title}
\n
\n
\n
\n ${movie.release_date}\n
\n
\n ${movie.has_cnsub ? '含中字磁鏈' : movie.magnets_count > 0 ? '含磁鏈' : '无磁鏈'}\n ${movie.new_magnets ? '今日新種' : ""}\n
\n
\n
\n `; })); return moviesHtml; } }; 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, "timers", new Map); __publicField(this, "insertStyle", (css => { if (css) { -1 === css.indexOf(""); $("head").append(css); } })); __publicField(this, "layerIndexStack", []); 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, event) { if (!url) throw new Error("未传入url"); shadeClose = shadeClose ?? !0; if (event && (event.ctrlKey || event.metaKey)) { GM_openInTab(url.includes("http") ? url : window.location.origin + url, { insert: 0 }); return; } let finalUrl = url; url.includes("/actors/") || url.includes("/star/") || (finalUrl = url.includes("?") ? `${url}&hideNav=1` : `${url}?hideNav=1`); layer.open({ type: 2, title: title, content: finalUrl, scrollbar: !1, shadeClose: shadeClose, area: this.getResponsiveArea([ "85%", "90%" ]), isOutAnim: !1, anim: -1, success: (layero, index) => { this.setupEscClose(index); } }); } _handleGlobalEscKey(e) { if ("Escape" !== e.key && 27 !== e.keyCode) return; if (0 === this.layerIndexStack.length) return; const topLayerIndex = this.layerIndexStack[this.layerIndexStack.length - 1], $layer = $(`#layui-layer${topLayerIndex}`); let viewerExists = !1; if ($layer.find(".viewer-container").length > 0) viewerExists = !0; else { const iframe = $layer.find(`#layui-layer-iframe${topLayerIndex}`)[0]; if (iframe && iframe.contentDocument) try { $(iframe.contentDocument).find(".viewer-container").length > 0 && (viewerExists = !0); } catch (error) { clog.warn("无法检查跨域 iframe 内的 .viewer-container"); } } if (!viewerExists) { this.layerIndexStack.pop(); layer.close(topLayerIndex); } } setupEscClose(layerIndex) { var _a2; if (!this._boundHandler) { this._boundHandler = this._handleGlobalEscKey.bind(this); $(document).off("keydown.globalLayerEsc"); $(document).on("keydown.globalLayerEsc", this._boundHandler); } -1 === this.layerIndexStack.indexOf(layerIndex) && this.layerIndexStack.push(layerIndex); const $iframe = $(`#layui-layer-iframe${layerIndex}`), eventNamespace = `keydown.layerEsc${layerIndex}`; try { const iframeDocument = null == (_a2 = $iframe[0]) ? void 0 : _a2.contentDocument; if (iframeDocument) { if ("yes" === $iframe.attr("data-esc-bound")) return; $(iframeDocument).off(eventNamespace); $(iframeDocument).on(eventNamespace, this._boundHandler); $iframe.attr("data-esc-bound", "yes"); } } catch (e) { clog.error("iframe监听失败 (跨域或未加载完毕):", e); } } closePage() { storageManager.getSetting("needClosePage", "yes").then((needClosePage => { if ("yes" !== needClosePage) return; 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) { const uuid = Math.random(), start = (new Date).getTime(), stopAndRun = shouldRun => { clearInterval(this.intervalContainer[uuid]); shouldRun && after && after(); delete this.intervalContainer[uuid]; }; this.intervalContainer[uuid] = setInterval((() => { const timeElapsed = (new Date).getTime() - start; condition() ? stopAndRun(!0) : timeElapsed >= timeout && stopAndRun(runWhenTimeout); }), detectInterval); } rightClick(container, targetSelector, callback) { let containerElement; "string" == typeof container ? containerElement = document.querySelector(container) : container instanceof HTMLElement && (containerElement = container); if (!containerElement) { console.warn("rightClick(), 容器无效或未提供,将使用 document.body 进行全局委托。"); containerElement = document.body; } "string" == typeof targetSelector && "" !== targetSelector.trim() ? containerElement.addEventListener("contextmenu", (event => { const targetElement = event.target.closest(targetSelector); targetElement && callback(event, targetElement); })) : console.error("rightClick(), 必须提供有效的 targetSelector。"); } q(event, msg, fun, cancelFun) { let x, y; if (event) { x = event.clientX - 130; y = event.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 && fun(); layer.close(confirmIndex); }), (function() { cancelFun && cancelFun(); })); } alert(event, msg, yesFun) { let offset; event && (offset = [ event.clientX - 200, event.clientY - 120 ]); let confirmIndex = layer.alert(msg, { offset: offset, shade: 0, zIndex: 999999991 }, (function() { yesFun && yesFun(); layer.close(confirmIndex); })); } 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)}`; } getHourDifference(date1, date2) { const timestamp1 = date1.getTime(), timestamp2 = date2.getTime(), differenceInHours = Math.abs(timestamp2 - timestamp1) / 36e5; return Math.floor(differenceInHours); } download(data, fileName) { show.info("开始请求下载..."); const fileExtension = fileName.split(".").pop().toLowerCase(); let blob, mimeType = this.mimeTypes[fileExtension] || "application/octet-stream"; if (data instanceof Blob) blob = data; else if (data instanceof ArrayBuffer || ArrayBuffer.isView(data)) blob = new Blob([ data ], { type: mimeType }); else if ("string" == typeof data && data.startsWith("data:")) { const byteString = atob(data.split(",")[1]), arrayBuffer = new ArrayBuffer(byteString.length), uintArray = new Uint8Array(arrayBuffer); for (let i = 0; i < byteString.length; i++) uintArray[i] = byteString.charCodeAt(i); blob = new Blob([ uintArray ], { type: mimeType }); } else blob = new Blob([ data ], { type: mimeType }); const 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 crypto.randomUUID().replace("-", ""); } 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()); } getUrlParam(url, key) { const searchString = url.split("?")[1]; if (!searchString) return null; const regex = new RegExp(`(?:^|&)${key}=([^&]*)`), match = searchString.match(regex); let value = ""; match && match[1] && (value = decodeURIComponent(match[1].replace(/\+/g, " "))); return value ? "true" === value || "false" === value ? "true" === value.toLowerCase() : "string" != typeof value || "" === value.trim() || isNaN(Number(value)) ? value : Number(value) : value; } reBuildSignature() { return javDbApi.buildSignature(); } getResponsiveArea(defaultArea) { const screenWidth = window.innerWidth; return screenWidth >= 1200 ? defaultArea || this.getDefaultArea() : screenWidth >= 768 ? [ "70%", "90%" ] : [ "95%", "95%" ]; } getDefaultArea() { return [ "85%", "90%" ]; } 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))); } copyToClipboard(type, text) { navigator.clipboard.writeText(text).then((() => show.info(`${type}已复制到剪切板, ${text}`))).catch((err => console.error("复制失败: ", err))); } htmlTo$dom(html) { const parser = new DOMParser; return $(parser.parseFromString(html, "text/html")); } addCookie(cookieStr, options = {}) { const {maxAge: maxAge = 604800, path: path = "/", domain: domain = "", secure: secure = !1, sameSite: sameSite = "Lax"} = options; cookieStr.split(";").forEach((cookie => { const trimmed = cookie.trim(); if (trimmed) { const parts = trimmed.split("="); if (parts.length >= 2 && parts[0].trim()) { let cookieParts = [ `${parts[0].trim()}=${parts.slice(1).join("=")}` ]; maxAge > 0 && cookieParts.push(`max-age=${maxAge}`); cookieParts.push(`path=${path}`); domain && cookieParts.push(`domain=${domain}`); secure && cookieParts.push("Secure"); sameSite && cookieParts.push(`SameSite=${sameSite}`); console.log("document.cookie = '" + cookieParts.join("; ") + "'"); document.cookie = cookieParts.join("; "); } } })); } isHidden(el) { const element = el.jquery ? el[0] : el; return !element || (element.offsetWidth <= 0 && element.offsetHeight <= 0 || "none" === window.getComputedStyle(element).display); } time(label = "default", unit = "s", precision = 2) { if (this.timers.has(label)) { const timer = this.timers.get(label), elapsedTime = performance.now() - timer.startTime; let formattedTime, unitLabel; if ("s" === timer.unit) { formattedTime = (elapsedTime / 1e3).toFixed(timer.precision); unitLabel = "秒"; } else { formattedTime = elapsedTime.toFixed(timer.precision); unitLabel = "毫秒"; } this.timers.delete(label); return `${label}: ${formattedTime}${unitLabel}`; } this.timers.set(label, { startTime: performance.now(), unit: unit, precision: precision }); } sleep(ms = 1e3) { return new Promise((resolve => setTimeout(resolve, ms))); } genericSort(arr, sortRules, emptyLast = !0) { if (!Array.isArray(arr) || 0 === arr.length) return []; if (!Array.isArray(sortRules) || 0 === sortRules.length) return [ ...arr ]; const sortedArr = [ ...arr ], safeDateConvert = value => { if (value instanceof Date) return value; if ("string" == typeof value) { const date = new Date(value); if (!isNaN(date.getTime())) return date; } return value; }; return sortedArr.sort(((a, b) => { for (const rule of sortRules) { const {key: key, order: order = "asc"} = rule; let valA = a, valB = b; if (null != key) if ("function" == typeof key) { valA = key(a); valB = key(b); } else { valA = a && "object" == typeof a ? a[key] : void 0; valB = b && "object" == typeof b ? b[key] : void 0; } const dateA = safeDateConvert(valA), dateB = safeDateConvert(valB); let comparison = 0; const aIsNull = null == valA, bIsNull = null == valB; if (aIsNull && bIsNull) return 0; if (aIsNull) return emptyLast ? 1 : -1; if (bIsNull) return emptyLast ? 1 : -1; comparison = dateA instanceof Date && dateB instanceof Date ? dateA.getTime() - dateB.getTime() : "number" == typeof valA && "number" == typeof valB ? valA - valB : "string" == typeof valA && "string" == typeof valB ? valA.localeCompare(valB) : String(valA).localeCompare(String(valB)); "desc" === order && (comparison *= -1); if (0 !== comparison) return comparison; } return 0; })); } async retry(fun, tryCount = 3) { let runCount = 0; for (;runCount < tryCount; ) try { const result = await fun(); runCount > 0 && clog.debug(`[重试] 成功,共发起 ${runCount + 1} 次。`); return result; } catch (e) { let errorString = String(e); errorString.startsWith("Error: ") && (errorString = errorString.replace("Error: ", "")); if (errorString.includes("Just a moment") || errorString.includes("重定向") || errorString.toLowerCase().includes("404 page not found") || errorString.toLowerCase().includes("404 not found")) throw e; runCount++; if (runCount === tryCount) { clog.debug(`[重试] 达到最大重试次数 (${tryCount}),最终失败:`, e); throw e; } clog.debug(`[重试] 准备第 ${runCount + 1} 次重试, 错误信息: ${errorString}`); } } copyObj(data) { return JSON.parse(JSON.stringify(data)); } deepFreeze(obj) { if (null === obj || "object" != typeof obj || Object.isFrozen(obj)) return obj; const propNames = Object.getOwnPropertyNames(obj); for (const name2 of propNames) { const value = obj[name2]; value && "object" == typeof value && this.deepFreeze(value); } return Object.freeze(obj); } } unsafeWindow.utils = window.utils = new Utils; unsafeWindow.gmHttp = window.gmHttp = new class { async get(url, params = {}, headers = {}, noRedirect) { return this.gmRequest("GET", url, null, params, headers, noRedirect); } post(url, data = {}, headers = {}) { headers = { "Content-Type": "application/json", ...headers }; let jsonData = JSON.stringify(data); return this.gmRequest("POST", url, jsonData, null, headers); } postForm(url, data = {}, headers = {}) { headers || (headers = {}); headers["Content-Type"] || (headers["Content-Type"] = "application/x-www-form-urlencoded"); let body = ""; data && Object.keys(data).length > 0 && (body = Object.entries(data).map((([key, value]) => `${key}=${value}`)).join("&")); return this.gmRequest("POST", url, body, null, headers); } postFileFormData(url, data = {}, headers = {}) { headers || (headers = {}); const boundary = `----WebKitFormBoundary${Math.random().toString(36).substring(2)}`; headers["Content-Type"] = `multipart/form-data; boundary=${boundary}`; let body = ""; data && Object.keys(data).length > 0 && (body = Object.entries(data).map((([key, value]) => `--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`)).join("")); body += `--${boundary}--`; return this.gmRequest("POST", url, body, null, headers); } async downloadFileInChunks(url, headers = {}) { const httpTimeout = await storageManager.getSetting("httpTimeout", 5e3), httpRetryCount = await storageManager.getSetting("httpRetryCount", 3); clog.log("正在获取文件大小..."); let fileSize, mimeType; try { const sizeResponse = await utils.retry((() => new Promise(((resolve, reject) => { GM_xmlhttpRequest({ method: "GET", url: url, headers: { ...headers, Range: "bytes=0-0" }, timeout: httpTimeout, onload: resolve, onerror: e => reject(new Error("网络错误:无法获取文件大小")), ontimeout: () => reject(new Error("超时:获取文件大小")) }); }))), httpRetryCount); if (206 !== sizeResponse.status && 200 !== sizeResponse.status) throw new Error(`请求文件大小失败,状态码: ${sizeResponse.status}`); { const rangeHeader = sizeResponse.responseHeaders.match(/content-range:\s*bytes\s*\d+-\d+\/(\d+)/i), typeHeader = sizeResponse.responseHeaders.match(/content-type:\s*([^\s;]+)/i); if (rangeHeader && rangeHeader[1]) fileSize = parseInt(rangeHeader[1], 10); else { if (!sizeResponse.responseHeaders.match(/content-length:\s*(\d+)/i) || 200 !== sizeResponse.status) throw new Error("无法从响应头中获取文件总大小,服务器可能不支持 Range 请求。"); { const lengthHeader = sizeResponse.responseHeaders.match(/content-length:\s*(\d+)/i); fileSize = parseInt(lengthHeader[1], 10); clog.warn("服务器返回 200 状态码,可能不支持 Range 请求。将尝试完整下载。"); } } typeHeader && typeHeader[1] && (mimeType = typeHeader[1]); clog.log(`文件总大小:${(fileSize / 1024 / 1024).toFixed(2)} MB, MIME 类型: ${mimeType || "未知"}`); } } catch (e) { clog.error("获取文件大小失败:", e.message); throw e; } if (!fileSize || fileSize <= 0) throw new Error("获取到的文件大小无效或服务器拒绝提供大小信息。"); const numChunks = Math.ceil(fileSize / 1048576), chunkPromises = [], downloadedChunks = new Array(numChunks); clog.log(`文件将被分为 ${numChunks} 块进行下载 (每块约 ${1..toFixed(2)} MB)`); for (let i = 0; i < numChunks; i++) { const start = 1048576 * i, rangeHeader = `bytes=${start}-${Math.min(start + 1048576 - 1, fileSize - 1)}`, chunkPromise = await utils.retry((() => new Promise(((resolve, reject) => { const currentHeaders = { ...headers, Range: rangeHeader, Accept: "application/octet-stream" }; GM_xmlhttpRequest({ method: "GET", url: url, headers: currentHeaders, timeout: httpTimeout, responseType: "arraybuffer", onload: response => { if (206 === response.status || 200 === response.status) if (response.response instanceof ArrayBuffer) { downloadedChunks[i] = response.response; clog.log(`成功下载第 ${i + 1}/${numChunks} 块 (${rangeHeader})`); resolve(); } else reject(new Error(`第 ${i + 1} 块响应不是 ArrayBuffer。`)); else reject(new Error(`第 ${i + 1} 块请求失败,状态码: ${response.status}`)); }, onerror: error => reject(new Error(`第 ${i + 1} 块网络错误: ${error.error}`)), ontimeout: () => reject(new Error(`第 ${i + 1} 块超时。`)) }); }))), httpRetryCount); chunkPromises.push(chunkPromise); } try { await Promise.all(chunkPromises); clog.log("所有分块下载完成,开始合并..."); } catch (e) { clog.error("分块下载过程中发生错误:", e.message); throw e; } const finalBlob = new Blob(downloadedChunks); finalBlob.size !== fileSize && clog.warn(`警告:合并后的 Blob 大小 (${finalBlob.size}) 与预期文件大小 (${fileSize}) 不匹配!`); return await finalBlob.text(); } async gmRequest(method, url, data = {}, params = {}, headers = {}, noRedirect = !1) { if (params && Object.keys(params).length) { const queryString = new URLSearchParams(params).toString(); url += (url.includes("?") ? "&" : "?") + queryString; } const httpTimeout = await storageManager.getSetting("httpTimeout", 5e3), httpRetryCount = await storageManager.getSetting("httpRetryCount", 3); data || (data = void 0); return await utils.retry((() => new Promise(((resolve, reject) => { GM_xmlhttpRequest({ method: method, url: url, headers: headers, timeout: httpTimeout, data: data, onload: response => { try { noRedirect && response.finalUrl !== url && reject("请求被重定向了,URL是:" + response.finalUrl); if (response.status >= 200 && response.status < 300) if (response.responseText) try { resolve(JSON.parse(response.responseText)); } catch (e) { resolve(response.responseText); } else resolve(response.responseText || response); else { clog.error("请求失败,状态码:", response.status, url); if (response.responseText) try { const errorData = JSON.parse(response.responseText); reject(errorData); } catch { reject(new Error(response.responseText || `请求发生错误 ${response.status}`)); } else reject(new Error(`请求发生错误 ${response.status}`)); } } catch (e) { reject(e); } }, onerror: error => { clog.error("网络错误:", url); reject(new Error(error.error || "网络错误")); }, ontimeout: () => { reject(new Error("请求超时: " + url)); } }); }))), httpRetryCount); } }; unsafeWindow.storageManager = window.storageManager = new StorageManager; const senderChannel = new BroadcastChannel("jhs-channel"); window.refresh = function() { senderChannel.postMessage({ type: "refresh" }); }; window.clean_cacheBlacklistCarList = function() { storageManager.cacheBlacklistCarList && (storageManager.cacheBlacklistCarList = null); senderChannel.postMessage({ type: "clean_cacheBlacklistCarList" }); }; window.clean_cacheSettingObj = function() { storageManager.cacheSettingObj && (storageManager.cacheSettingObj = null); senderChannel.postMessage({ type: "clean_cacheSettingObj" }); }; window.clean_cacheFavoriteActressList = function() { storageManager.cacheFavoriteActressList && (storageManager.cacheFavoriteActressList = null); senderChannel.postMessage({ type: "clean_cacheFavoriteActressList" }); }; window.clean_cacheCarList = function() { storageManager.cacheCarList && (storageManager.cacheCarList = null); senderChannel.postMessage({ type: "clean_cacheCarList" }); }; new BroadcastChannel("jhs-channel").addEventListener("message", (async event => { let dataType = event.data.type; if ("refresh" === dataType) { const listPagePlugin = window.pluginManager.getBean("ListPagePlugin"); await listPagePlugin.doFilter(); const historyPlugin = window.pluginManager.getBean("HistoryPlugin"); historyPlugin.tableObj && historyPlugin.tableObj.setData(); const newVideoPlugin = window.pluginManager.getBean("NewVideoPlugin"); if (newVideoPlugin) { newVideoPlugin.showNewVideoCount().then(); newVideoPlugin.loadData(); } } else "clean_cacheBlacklistCarList" === dataType ? storageManager.cacheBlacklistCarList && (storageManager.cacheBlacklistCarList = null) : "clean_cacheSettingObj" === dataType ? storageManager.cacheSettingObj && (storageManager.cacheSettingObj = null) : "clean_cacheFavoriteActressList" === dataType ? storageManager.cacheFavoriteActressList && (storageManager.cacheFavoriteActressList = null) : "clean_cacheCarList" === dataType && storageManager.cacheCarList && (storageManager.cacheCarList = null); })); !function() { document.head.insertAdjacentHTML("beforeend", '\n \n '); unsafeWindow.loading = window.loading = function() { const container = document.createElement("div"); container.className = "loading-container"; const animation = document.createElement("div"); animation.className = "loading-animation"; const closeBtn = document.createElement("button"); closeBtn.className = "loading-close-btn"; closeBtn.textContent = "关闭"; closeBtn.addEventListener("click", (() => { close(); })); setTimeout((() => { closeBtn.classList.add("visible"); }), 3e3); container.appendChild(animation); container.appendChild(closeBtn); document.body.appendChild(container); const close = () => { container && container.parentNode && container.parentNode.removeChild(container); }; return { close: close }; }; }(); !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: 1e3, 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 }; -1 === defaultConfig.duration && (defaultConfig.close = !0); const toast = Toastify(defaultConfig); toast.showToast(); toast.closeShow = () => { toast.toastElement.remove(); }; return toast; }; unsafeWindow.show = 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) }; }(); !function() { document.head.insertAdjacentHTML("beforeend", "\n \n "); function smartToggleBodyScroll2(waitTime = 10) { setTimeout((() => { const openLayerCount = document.querySelectorAll(".layui-layer-shade").length; document.documentElement.style.overflow = openLayerCount > 0 ? "hidden" : ""; }), waitTime); } window.showImageViewer = function(imgUrlOrContainer, altText = "") { let $container = null, isTempContainer = !1; if ("string" == typeof imgUrlOrContainer || imgUrlOrContainer instanceof String) { $container = $('\n `; document.body.appendChild(panel); const header = document.getElementById("jhs-cookie-header"); document.getElementById("jhs-cookie-content"); const toggleIcon = document.getElementById("jhs-toggle-icon"), copyButton = document.getElementById("jhs-copy-btn"); header.addEventListener("click", (() => { const isExpanded = panel.classList.toggle("expanded"); toggleIcon.textContent = isExpanded ? "▲" : "▼"; })); copyButton.addEventListener("click", (async e => { e.stopPropagation(); try { await navigator.clipboard.writeText(cookieValue); this.showMessage("Cookie 已成功复制到剪贴板!"); } catch (err) { console.error("Failed to copy text using clipboard API: ", err); const tempTextArea = document.createElement("textarea"); tempTextArea.value = cookieValue; document.body.appendChild(tempTextArea); tempTextArea.select(); document.execCommand("copy"); document.body.removeChild(tempTextArea); this.showMessage("Cookie 已复制! (回退方案)"); } })); panel.classList.remove("expanded"); } } const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "loginStatus", _WangPan115MatchPlugin.LoginStatus.UNCHECKED); } getName() { return "WangPan115MatchPlugin"; } async initCss() { return "\n \n "; } async handle() { $(document).on("click", ".jhs-match-no-login-btn", (async event => { event.preventDefault(); event.stopPropagation(); await this.handleLoginRedirect(); })); $(document).on("click", ".jhs-match-btn", (event => { event.preventDefault(); event.stopImmediatePropagation(); this.showMatchDetail(event.currentTarget); })); $(document).on("click", ".jhs-match-error-btn", (async event => { event.preventDefault(); event.stopPropagation(); await this.retryMatch(event.currentTarget); })); await this.matchDetailPage(); $(document).on("click", ".jhs-match-detail-error-btn", (async event => { event.preventDefault(); event.stopPropagation(); $(event.currentTarget).replaceWith("匹配中..."); try { const carNum2 = this.getPageInfo().carNum, matchList = await this.searchFiles(carNum2); $(".jhs-115-match-detail").remove(); await this.matchDetailPage(matchList); } catch (error) { console.error(`重新匹配失败 [${carNum}]:`, error); this.showMatchError($box, carNum, error); } })); } async matchDetailPage(matchList) { if (!isDetailPage) return; if (await storageManager.getSetting("enable115Match", NO) === NO) return; const $detail = $('\n
\n \n \n \n \n \n \n \n \n \n \n \n \n \n \n
115匹配
名称大小时间播放
\n
\n '), $tbody = $detail.find("tbody"); try { const carNum2 = this.getPageInfo().carNum; matchList || (matchList = await this.searchFiles(carNum2)); await this.checkLoginStatus(); if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $tbody.append(`\n 未登录\n `); else if (matchList.length > 0) { const rowsHtml = matchList.map((match => `\n \n ${match.name}\n ${this.formatSize(match.size)}\n ${match.createTime}\n \n 播放\n \n \n `)).join(""); $tbody.append(rowsHtml); } else $tbody.append(`\n 未匹配\n `); } catch (error) { $tbody.append(`\n 加载失败,请重试\n `); console.error("加载文件列表时发生错误:", error); } if (isJavDb) if ($("#all-match-box").length) $("#all-match-box").append($detail); else { $("#tabs-container").before("
"); $("#all-match-box").append($detail); } else $("#mag-submit-show").before($detail); } async matchMovieList(movieListElement) { if (await storageManager.getSetting("enable115Match", NO) !== NO) { await this.checkLoginStatus(); await this.processMovieElements(movieListElement); } else $(".video-title [class^='jhs-match-']").remove(); } showMatchDetail(buttonElement) { const $el = $(buttonElement), matchData = $el.attr("data-match"); $(".jhs-match-detail").remove(); const matches = this.parseMatchData(matchData); if (0 === matches.length) return; if (1 === matches.length) { const pickcode = matches[0].videoId; window.open(`https://115vod.com/?pickcode=${pickcode}&share_id=0`, "_blank"); return; } const $detail = this.createMatchDetailElement(matches); this.positionDetailElement($detail, $el); this.addOutsideClickHandler($detail); $detail.on("click", (e => { e.stopPropagation(); })); } parseMatchData(matchData) { try { return JSON.parse(matchData) || []; } catch (e) { console.error("解析匹配数据失败:", e); return []; } } createMatchDetailElement(matches) { const $detail = $(`\n
\n \n \n \n \n \n \n \n \n \n \n ${matches.map((match => `\n \n \n \n \n \n \n `)).join("")}\n \n
名称大小时间播放
${match.name}${this.formatSize(match.size)}${match.createTime}\n 播放\n
\n
\n `); $("body").append($detail); return $detail; } positionDetailElement($detail, $trigger) { const offset = $trigger.offset(); $detail.css({ top: offset.top - $detail.outerHeight() + 20, left: offset.left }); } addOutsideClickHandler($detail) { setTimeout((() => { $(document).on("click.jhs-match-detail", (e => { if (!$detail.is(e.target) && 0 === $detail.has(e.target).length) { $detail.remove(); $(document).off("click.jhs-match-detail"); } })); }), 100); } async retryMatch(buttonElement) { const $el = $(buttonElement), $box2 = $el.closest(".movie-box, .item"), carNum2 = $el.attr("data-keyword"); $el.replaceWith("匹配中..."); try { const matchList = await this.searchFiles(carNum2); this.updateMatchStatus($box2, carNum2, matchList); } catch (error) { console.error(`重新匹配失败 [${carNum2}]:`, error); this.showMatchError($box2, carNum2, error); } } updateMatchStatus($box2, carNum2, matchList) { matchList.length > 0 ? $box2.find(".jhs-match-btn").replaceWith(`匹配${matchList.length}个`) : $box2.find(".jhs-match-btn").replaceWith(`未匹配`); } async handleLoginRedirect() { window.open("https://115.com"); } async searchFiles(carNum2) { var _a2; let searchKeyword = carNum2.toLowerCase().replace("fc2-", ""); return (null == (_a2 = (await searchFiles(searchKeyword)).data) ? void 0 : _a2.map((data => ({ folderId: data.fid, videoId: data.pc, name: data.n, createTime: utils.formatDate(new Date(1e3 * data.te)), size: data.s, isVideo: [ ".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv" ].some((ext => { var _a3; return null == (_a3 = data.n) ? void 0 : _a3.toLowerCase().endsWith(ext); })) }))).filter((x => x.folderId && x.isVideo && x.name.toLowerCase().includes(searchKeyword)))) || []; } showMatchError($box2, carNum2, error) { $box2.find(".jhs-match-btn").replaceWith(`匹配失败`); show.error(`${carNum2} 匹配失败: ${error.message || "网络错误"}`); } async checkLoginStatus() { var _a2; if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.UNCHECKED) try { const testResult = await searchFiles("test"); this.loginStatus = (null == (_a2 = testResult.error) ? void 0 : _a2.includes("登录")) ? _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT : _WangPan115MatchPlugin.LoginStatus.LOGGED_IN; } catch { this.loginStatus = _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT; } } async processMovieElements(movieListElement) { const promises = Array.from(movieListElement).filter((ele => !utils.isHidden(ele))).filter((ele => !(isJavBus && $(ele).find(".avatar-box").length > 0))).map((ele => this.processSingleMovieElement(ele))); await Promise.all(promises); } async processSingleMovieElement(element) { const $box2 = $(element), {carNum: carNum2} = this.getBoxCarInfo($box2); if (!($box2.find("[class^='jhs-match-']").length > 0)) if (this.loginStatus !== _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) try { const matchList = await this.searchFiles(carNum2); this.addTag($box2, carNum2, matchList); } catch (error) { console.error(`搜索失败 [${carNum2}]:`, error); this.addTag($box2, carNum2, []); } else this.addTag($box2, carNum2, []); } addTag($box2, carNum2, matchList) { if (!($box2.find("[class^='jhs-match-']").length > 0)) if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $box2.find(".video-title").prepend(`未登录`); else if (matchList.length > 0) { const title = 1 === matchList.length ? "点击直接播放" : `点击查看${matchList.length}个匹配结果`; $box2.find(".video-title").prepend(`匹配${matchList.length}个`); } else $box2.find(".video-title").prepend(`未匹配`); } formatSize(bytes) { if (!bytes) return "-"; const units = [ "B", "KB", "MB", "GB", "TB" ]; let size = parseFloat(bytes), unit = 0; for (;size >= 1024 && unit < units.length - 1; ) { size /= 1024; unit++; } return `${size.toFixed(2)} ${units[unit]}`; } }; __publicField(_WangPan115MatchPlugin, "LoginStatus", { UNCHECKED: -1, LOGGED_OUT: 0, LOGGED_IN: 1 }); let WangPan115MatchPlugin = _WangPan115MatchPlugin; class FavoriteActressesPlugin extends BasePlugin { getName() { return "FavoriteActressesPlugin"; } async handle() { this.bindEvent(); await this.highlightActress(); this.replaceActressAvatar(); } async highlightActress() { if (!isDetailPage) return; if (await storageManager.getSetting("enableFavoriteActresses", YES) !== YES) return; const favoriteActressesInfoList = await storageManager.getFavoriteActressList(); if (!favoriteActressesInfoList || 0 === favoriteActressesInfoList.length) return; const favoriteStarIdSet = new Set; favoriteActressesInfoList.forEach((actress => { actress.starId && favoriteStarIdSet.add(String(actress.starId).trim()); })); 0 !== favoriteStarIdSet.size && $(".female").prev().each(((index, element) => { const $element = $(element), href = $element.attr("href"); let elementStarId = null; if (href) { const parts = (href.endsWith("/") ? href.slice(0, -1) : href).split("/"), lastPart = parts[parts.length - 1]; lastPart && (elementStarId = lastPart.trim()); } let isFavorite = !1; elementStarId && (isFavorite = favoriteStarIdSet.has(elementStarId)); if (isFavorite) { $element.addClass("highlighted"); $element.attr("title", "高亮已收藏演员, 可在设置-基础配置中关闭"); } })); } async removeActorFromStorage(actorId) { await storageManager.removeFavoriteActress(actorId) && clog.log("移除演员成功"); } bindEvent() { const listIdRegex = /\/actors\/(\w+)\/(collect|uncollect)/; $(document).on("confirm:complete", 'a[href*="/actors/"][href*="/uncollect"]', (async event => { const [userConfirmed] = event.detail; if (!userConfirmed) return; const match = $(event.currentTarget).attr("href").match(listIdRegex), actorId = match ? match[1] : null; actorId && await this.removeActorFromStorage(actorId); })); $("#button-collect-actor").click((async event => { const match = $("#button-collect-actor").attr("href").match(listIdRegex), actorId = match ? match[1] : null; let nameList = [], $actor = $(".actor-section-name"); $actor.length && $actor.text().trim().split(",").forEach((name2 => { nameList.push(name2.trim()); })); let $sectionMeta = $(".section-meta:not(:contains('影片'))"); $sectionMeta.length && $sectionMeta.text().trim().split(",").forEach((name2 => { nameList.push(name2.trim()); })); if (!nameList) { clog.error("获取演员名称失败"); return; } const actorName = nameList[0]; if (!actorId) { clog.error("无法获取演员ID进行收藏操作。"); return; } const avatar = ($(".avatar").first().css("background-image") || "").replace(/^url\(["']?|["']?\)$/g, ""), actressInfo = { starId: actorId, name: actorName, allName: nameList, avatar: avatar }; 1 === await storageManager.addFavoriteActressList([ actressInfo ]) ? clog.log(`收藏演员成功: ${actorName} (ID: ${actorId})`) : clog.log(`收藏演员失败: ${actorName} (ID: ${actorId})`); })); $("#button-uncollect-actor").click((async event => { const match = $("#button-uncollect-actor").attr("href").match(listIdRegex), actorId = match ? match[1] : null; actorId ? await this.removeActorFromStorage(actorId) : clog.error("无法获取演员ID进行取消收藏操作。"); })); } async replaceActressAvatar() { const actressId = this.getActressId(); if (!actressId) return; const actress = (await storageManager.getFavoriteActressList()).find((item => item.starId === actressId)); if (actress && actress.avatar) { const newAvatarUrl = `url('${actress.avatar}')`; let $avatarElement = $(".avatar").first(); if (0 === $avatarElement.length) { const newAvatarHtml = '
'; $(".section-columns").prepend(newAvatarHtml); $avatarElement = $(".avatar").first(); } if (0 === $avatarElement.length) return; if ($avatarElement.css("background-image").trim().toLowerCase() !== newAvatarUrl.trim().toLowerCase()) { $avatarElement.css("background-image", newAvatarUrl); $avatarElement.css("background-size", "cover"); $avatarElement.css("background-position", "top center"); $avatarElement.css("background-repeat", "no-repeat"); } } } } class BusImgPlugin extends BasePlugin { getName() { return "BusImgPlugin"; } handle() {} async getVisibleImageItems(itemSelector, imgSelector) { let itemList = []; const allItems = document.querySelectorAll(itemSelector); for (const item of allItems) { if (!utils.isHidden(item)) { const imgElement = item.querySelector(imgSelector); if (!(imgElement instanceof HTMLImageElement)) continue; imgElement.style.removeProperty("height"); let imageHeight = imgElement.offsetHeight; imageHeight > 0 && itemList.push({ element: item, imgElement: imgElement, height: imageHeight }); } } return itemList; } async logImageHeightsByRow() { if (await storageManager.getSetting("enableVerticalModel", NO) === YES) return; const itemSelector = this.getSelector().itemSelector, containerColumns = await storageManager.getSetting("containerColumns", 5), itemList = await this.getVisibleImageItems(itemSelector, "img"); if (0 === itemList.length) return; const groupedItems = []; for (let i = 0; i < itemList.length; i++) { const rowIndex = Math.floor(i / containerColumns); groupedItems[rowIndex] || (groupedItems[rowIndex] = []); groupedItems[rowIndex].push(itemList[i]); } groupedItems.forEach(((row, rowIndex) => { const originalHeights = row.map((item => item.height)); if (originalHeights.length < 2) return; const minHeight = Math.min(...originalHeights), maxHeight = Math.max(...originalHeights); let targetHeight = 0; if (maxHeight - minHeight > 50) { targetHeight = minHeight; row.forEach((item => { if (item.height !== targetHeight) { const heightValue = `${targetHeight}px`; item.imgElement.style.setProperty("height", heightValue, "important"); } })); } })); } } class TranslatePlugin extends BasePlugin { getName() { return "TranslatePlugin"; } async initCss() { return "\n \n "; } handle() { isDetailPage && this.translate(); } async translate(carNum2, showCarNum = !0) { if (await storageManager.getSetting("translateTitle", YES) !== YES) return; isJavBus && (showCarNum = !1); let $titleElement = $(".origin-title"); $titleElement.length || ($titleElement = $(".current-title")); $titleElement.length || ($titleElement = $("h3")); if (!$titleElement.length) return; const originalText = $titleElement.text().trim(); if (!originalText) { show.error("获取标题失败, 无法进行翻译"); return; } $titleElement.after('
翻译中...
'); const $loadingElement = $titleElement.next(".translated-title"); carNum2 || (carNum2 = this.getPageInfo().carNum); const cache = localStorage.getItem("jhs_translate") ? JSON.parse(localStorage.getItem("jhs_translate")) : {}; cache[carNum2] ? $loadingElement.html(showCarNum ? carNum2 + "   " + cache[carNum2] : cache[carNum2]) : translateText(originalText, "ja", "zh-CN").then((translatedText => { $loadingElement.html(showCarNum ? carNum2 + "   " + translatedText : translatedText); })).catch((error => { console.error("翻译失败:", error); $loadingElement.replaceWith(`
翻译失败: ${error.message}
`); })); } } class TaskPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "singleTaskKey", "checkNewActressActorFilterCar"); __publicField(this, "taskConfig", null); __publicField(this, "storageQueue", new AsyncQueue); __publicField(this, "lastCheckFavoriteActressTimeKey", "jhs_time_checkFavoriteActress"); __publicField(this, "lastCheckBlacklistTimeKey", "jhs_time_checkBlacklist"); __publicField(this, "lastCheckNewVideoTimeKey", "jhs_time_checkNewVideo"); } getName() { return "TaskPlugin"; } async limitConcurrency(taskParamList, checkConcurrencyCount, checkRequestSleep, taskFn) { this.showIsRun(); const activePromises = [], totalItems = taskParamList.length; let processedCount = 0; for (const taskParam of taskParamList) { const p = taskFn(taskParam).finally((() => { activePromises.splice(activePromises.indexOf(p), 1); })); activePromises.push(p); processedCount++; if (activePromises.length >= checkConcurrencyCount) { const remainingToStart = totalItems - processedCount; clog.debug(`剩余任务数: ${remainingToStart}`); await Promise.race(activePromises); await utils.sleep(checkRequestSleep); } } await Promise.all(activePromises); } isUnnecessaryCheck(lastCheckTimeStr, checkIntervalTime) { if (!checkIntervalTime) throw new Error("未传入checkIntervalTime"); checkIntervalTime = parseInt(checkIntervalTime); return utils.getHourDifference(new Date(lastCheckTimeStr), new Date) < checkIntervalTime; } handle() { this.doTask().then(); } showIsRun() { show.info("正在执行检测任务中, 请勿关闭当前窗口", { duration: 3e3 }); } async doTask() { if (isListPage) { await this.loadConfig(); this.javDbUrl = await this.getBean("OtherSitePlugin").getJavDbUrl(); return navigator.locks.request(this.singleTaskKey, { ifAvailable: !0 }, (async lock => { if (lock) { if (isListPage) { this.taskConfig.enableCheckBlacklist === YES ? await this.checkBlacklist() : clog.warn("自动检测屏蔽黑名单-禁用"); if (!isJavBus) { if (this.taskConfig.enableCheckFavoriteActress === YES) { const lastCheckFavoriteActressTimeStr = localStorage.getItem(this.lastCheckFavoriteActressTimeKey), checkFavoriteActress_IntervalTime = this.taskConfig.checkFavoriteActress_IntervalTime, isUnCheck = lastCheckFavoriteActressTimeStr && this.isUnnecessaryCheck(lastCheckFavoriteActressTimeStr, checkFavoriteActress_IntervalTime), isLogin = $('a[href*="/users/profile"]').length > 0; isUnCheck && clog.debug(`检测同步演员, 上次检测时间: ${lastCheckFavoriteActressTimeStr} 检测间隔时间: ${checkFavoriteActress_IntervalTime}小时 未到时间`); !isUnCheck && isLogin && await this.checkFavoriteActress(); } else clog.warn("自动同步已收藏的演员-禁用"); this.taskConfig.enableCheckNewVideo === YES ? await this.checkNewVideo() : clog.warn("自动检测已收藏演员的最新作品-禁用"); } } } else clog.debug("争夺任务锁失败, 跳过执行"); })).catch((error => { console.error("锁任务出现错误:", error); clog.error("锁任务出现错误:", error); })).finally((() => { setTimeout((() => { this.doTask(); }), 3e5); })); } } async loadConfig() { const settingObj = await storageManager.getSetting(); this.taskConfig = { checkConcurrencyCount: settingObj.checkConcurrencyCount ? Number(settingObj.checkConcurrencyCount) : 2, checkRequestSleep: settingObj.checkRequestSleep ? Number(settingObj.checkRequestSleep) : 100, enableCheckBlacklist: settingObj.enableCheckBlacklist || YES, checkBlacklist_intervalTime: settingObj.checkBlacklist_intervalTime ? Number(settingObj.checkBlacklist_intervalTime) : 12, checkBlacklist_ruleTime: settingObj.checkBlacklist_ruleTime ? Number(settingObj.checkBlacklist_ruleTime) : 8760, enableCheckFavoriteActress: settingObj.enableCheckFavoriteActress || YES, checkFavoriteActress_IntervalTime: settingObj.checkFavoriteActress_IntervalTime ? Number(settingObj.checkFavoriteActress_IntervalTime) : 24, enableCheckNewVideo: settingObj.enableCheckNewVideo || YES, checkNewVideo_intervalTime: settingObj.checkNewVideo_intervalTime ? Number(settingObj.checkNewVideo_intervalTime) : 12, checkNewVideo_ruleTime: settingObj.checkNewVideo_ruleTime ? Number(settingObj.checkNewVideo_ruleTime) : 8760 }; } async checkBlacklist(isManual) { let blacklist = await storageManager.getBlacklist(); if (0 === blacklist.length) return; blacklist = blacklist.sort(((a, b) => a.createTime < b.createTime ? 1 : a.createTime > b.createTime ? -1 : 0)); const checkConcurrencyCount = this.taskConfig.checkConcurrencyCount, checkRequestSleep = this.taskConfig.checkRequestSleep, checkBlacklist_intervalTime = this.taskConfig.checkBlacklist_intervalTime, checkBlacklist_ruleTime = this.taskConfig.checkBlacklist_ruleTime, lastCheckBlacklistTimeStr = localStorage.getItem(this.lastCheckBlacklistTimeKey); if (!isManual && lastCheckBlacklistTimeStr && this.isUnnecessaryCheck(lastCheckBlacklistTimeStr, checkBlacklist_intervalTime)) { clog.debug(`检测黑名单, 上次检测时间: ${lastCheckBlacklistTimeStr} 检测间隔时间: ${checkBlacklist_intervalTime}小时 未到时间`); return; } const taskParamList = [], msgList = []; for (const blacklistItem of blacklist) { let name2 = blacklistItem.name, checkTime = blacklistItem.checkTime, lastPublishTime = blacklistItem.lastPublishTime, url = blacklistItem.url; if (new URL(window.location.href).hostname === new URL(url).hostname) { if (isManual || !checkTime || !this.isUnnecessaryCheck(checkTime, checkBlacklist_intervalTime)) if (!lastPublishTime || 0 === checkBlacklist_ruleTime || this.isUnnecessaryCheck(lastPublishTime, checkBlacklist_ruleTime)) taskParamList.push(blacklistItem); else { let msg = `检测黑名单: ${name2} ${lastPublishTime} 停更超过${checkBlacklist_ruleTime / 24 / 365}年,跳过检测`; msgList.push(msg); $("#checkBlacklistMsg").text(msg); } } else clog.log("黑名单地址非同域名,跳过", url); } if (0 === taskParamList.length) return; msgList.forEach((msg => { clog.log(msg); })); clog.log(`检测屏蔽黑名单, 总任务数: ${taskParamList.length}, 并发限制:${checkConcurrencyCount}, 请求间隔时间:${checkRequestSleep}ms`); const blacklistPlugin = this.getBean("BlacklistPlugin"); await this.limitConcurrency(taskParamList, checkConcurrencyCount, checkRequestSleep, (async task => { let {starId: starId, name: name2, url: url} = task; try { clog.log("正在检屏黑名单演员:", name2, url); $("#checkBlacklistMsg").text(`正在检屏黑名单演员: ${name2} ${url}`); const html = await gmHttp.get(url), $dom = utils.htmlTo$dom(html); this.storageQueue.addTask((async () => { let {lastPublishTime: lastPublishTime} = await blacklistPlugin.parseAndSaveFilterInfo($dom, name2, starId); await storageManager.updateBlacklistItem({ starId: starId, name: name2, checkTime: utils.getNowStr(), lastPublishTime: lastPublishTime }); })); } catch (e) { $("#checkBlacklistMsg").text(`检测屏蔽演员信息, 发生错误: ${url}`); clog.error("检测屏蔽演员信息, 发生错误:", url, e); show.error("检测屏蔽演员信息, 发生错误:" + e, "bottom", "right"); } })); await this.storageQueue.waitAllFinished(); const updateLastCheckBlacklistTimeStr = utils.getNowStr(); localStorage.setItem(this.lastCheckBlacklistTimeKey, updateLastCheckBlacklistTimeStr); clog.log('-------- END 检测屏蔽黑名单 END --------'); $("#checkBlacklistMsg").text("检测屏蔽黑名单, 结束"); this.getBean("BlacklistPlugin").resetBtnTip().then(); } async checkFavoriteActress() { const checkUrl = `${this.javDbUrl}/users/collection_actors`, actorInfoList = []; await this.scrapeActorInfo(checkUrl, actorInfoList); clog.log("所有演员信息已收集, 总计数量:", actorInfoList.length); $("#checkNewVideoMsg").text("同步完成"); if (actorInfoList.length > 0) { await storageManager.addFavoriteActressList(actorInfoList); localStorage.setItem(this.lastCheckFavoriteActressTimeKey, utils.getNowStr()); this.getBean("NewVideoPlugin").resetBtnTip().then(); } } async scrapeActorInfo(currentUrl, actorInfoList) { clog.log(`正在抓取页面: ${currentUrl}`); $("#checkNewVideoMsg").text(`正在解析已收藏的演员: ${currentUrl}`); try { const html = await gmHttp.get(currentUrl), $dom = utils.htmlTo$dom(html); $dom.find("#actors .actor-box a").each(((i, element) => { const $a = $(element), title = $a.attr("title"), href = $a.attr("href"); if (title && href) { const allName = title.split(",").map((name3 => name3.trim())).filter((name3 => name3.length > 0)), name2 = allName[0] || "", segments = new URL(href, this.javDbUrl).pathname.split("/").filter((s => s.length > 0)); let starId = ""; segments.length > 0 && (starId = segments[segments.length - 1]); let actressType = "censored"; const avatar = $a.find("img").attr("src"), $infoSpan = $a.find(".info"); $infoSpan.length && $infoSpan.text().trim().includes("無碼") && (actressType = "uncensored"); actorInfoList.push({ starId: starId, name: name2, allName: allName, avatar: avatar, actressType: actressType, lastCheckTime: null, lastUpdateTime: null }); } })); const nextRelativeUrl = $dom.find(".pagination-next").attr("href"); if (nextRelativeUrl) { const nextAbsoluteUrl = new URL(nextRelativeUrl, this.javDbUrl).href; await this.scrapeActorInfo(nextAbsoluteUrl, actorInfoList); } } catch (error) { clog.error(`抓取 ${currentUrl} 时发生错误:`, error); } } async checkNewVideo(isManual) { const dataActressInfoList = await storageManager.getFavoriteActressList(), actressInfoList = utils.genericSort(dataActressInfoList, [ { key: item => { var _a2; return (null == (_a2 = item.newVideoList) ? void 0 : _a2.length) ?? 0; }, order: "desc" }, { key: "lastPublishTime", order: "desc" } ]), checkConcurrencyCount = this.taskConfig.checkConcurrencyCount, checkRequestSleep = this.taskConfig.checkRequestSleep, checkNewVideo_intervalTime = this.taskConfig.checkNewVideo_intervalTime, checkNewVideo_ruleTime = this.taskConfig.checkNewVideo_ruleTime, lastCheckNewVideoTimeStr = localStorage.getItem(this.lastCheckNewVideoTimeKey); if (!isManual && lastCheckNewVideoTimeStr && this.isUnnecessaryCheck(lastCheckNewVideoTimeStr, checkNewVideo_intervalTime)) { clog.debug(`检测新作品, 上次检测时间: ${lastCheckNewVideoTimeStr} 检测间隔时间: ${checkNewVideo_intervalTime}小时 未到时间`); return; } const taskParamList = [], msgList = []; for (const actress of actressInfoList) { const {lastCheckTime: lastCheckTime, lastPublishTime: lastPublishTime, name: name2} = actress; !isManual && lastCheckTime && this.isUnnecessaryCheck(lastCheckTime, checkNewVideo_intervalTime) || (!lastPublishTime || 0 === checkNewVideo_ruleTime || this.isUnnecessaryCheck(lastPublishTime, checkNewVideo_ruleTime) ? taskParamList.push(actress) : msgList.push(`检测新作品: ${name2} ${lastPublishTime} 停更超过${checkNewVideo_ruleTime / 24 / 365}年,跳过检测`)); } if (0 === taskParamList.length) return; msgList.forEach((msg => { clog.log(msg); })); clog.log(`检测最新作品, 总任务数: ${taskParamList.length}, 并发限制:${checkConcurrencyCount}, 请求间隔时间:${checkRequestSleep}ms`); const filterKeywordList = await storageManager.getTitleFilterKeyword(), filterActorActressCarList = await storageManager.getBlacklistCarList(), filterActorActressCarNumList = new Set(filterActorActressCarList.map((car => car.carNum))); await this.limitConcurrency(taskParamList, checkConcurrencyCount, checkRequestSleep, (async task => { const {lastCheckTime: lastCheckTime, name: name2, starId: starId} = task; let url = `${this.javDbUrl}/actors/${starId}?t=d`; try { clog.log("正在检测最新作品, 演员:", name2, url); $("#checkNewVideoMsg").text(`正在检测最新作品, 演员: ${name2}`); const html = await gmHttp.get(url), $dom = utils.htmlTo$dom(html); this.storageQueue.addTask((async () => { await this.parsePage($dom, starId, name2, filterKeywordList, filterActorActressCarNumList); })); } catch (e) { clog.error("检测屏蔽演员信息, 发生错误:", url, e); console.error("检测屏蔽演员信息, 发生错误:", url, e); show.error("检测屏蔽演员信息, 发生错误:" + e, "bottom", "right"); } })); await this.storageQueue.waitAllFinished(); localStorage.setItem(this.lastCheckNewVideoTimeKey, utils.getNowStr()); clog.log('检测最新作品---结束'); $("#checkNewVideoMsg").text("检测完毕"); const newVideoPlugin = this.getBean("NewVideoPlugin"); newVideoPlugin.loadData(); newVideoPlugin.resetBtnTip().then(); } async parsePage($dom, starId, name2, filterKeywordList, filterActorActressCarNumList) { let movieList, nextPageLink, tempIsJavBus = !1, selectorType = "javdb"; if ($dom.text().includes("javbus")) { tempIsJavBus = !0; selectorType = "javbus"; } tempIsJavBus && $dom.find(".avatar-box").length > 0 && $dom.find(".avatar-box").parent().remove(); movieList = $dom.find(this.getSelector(selectorType).requestDomItemSelector); nextPageLink = $dom.find(this.getSelector(selectorType).nextPageSelector).attr("href"); if (nextPageLink && 0 === movieList.length) { clog.error("新作品检测-解析列表失败"); show.error("新作品检测-解析列表失败"); throw new Error("新作品检测-解析列表失败"); } let carNumList = [], lastPublishTime = null; for (const element of movieList) { const $item = $(element), {carNum: carNum2, url: url, title: title, publishTime: publishTime} = this.getBoxCarInfo($item); if (!carNum2) continue; if (!filterKeywordList.find((kw => title.includes(kw) || carNum2.startsWith(kw))) && !filterActorActressCarNumList.has(carNum2)) { lastPublishTime || (lastPublishTime = publishTime); carNumList.push(carNum2); } } const carList = await storageManager.getCarList(), storageCarNumList = new Set(carList.map((car => car.carNum))), nonExistingCarNumList = carNumList.filter((carNum2 => !storageCarNumList.has(carNum2))); nonExistingCarNumList.length > 0 && clog.log(`检测出新作品, ${name2}, 共${nonExistingCarNumList.length}部`); await storageManager.updateFavoriteActress({ starId: starId, lastCheckTime: utils.getNowStr(), newVideoList: nonExistingCarNumList, lastPublishTime: lastPublishTime }); } async checkOneNewVideo(actress) { const filterKeywordList = await storageManager.getTitleFilterKeyword(), filterActorActressCarList = await storageManager.getBlacklistCarList(), filterActorActressCarNumList = new Set(filterActorActressCarList.map((car => car.carNum))), {lastCheckTime: lastCheckTime, name: name2, starId: starId} = actress; let url = `${this.javDbUrl}/actors/${starId}?t=d`; const $checkNewVideoMsg = $("#checkNewVideoMsg"); try { clog.log("正在检测最新作品, 演员:", name2, url); $checkNewVideoMsg.text(`正在检测最新作品, 演员: ${name2}`); const html = await gmHttp.get(url), $dom = utils.htmlTo$dom(html); await this.parsePage($dom, starId, name2, filterKeywordList, filterActorActressCarNumList); clog.log('检测最新作品---结束'); $checkNewVideoMsg.text("检测完毕"); this.getBean("NewVideoPlugin").loadData(); } catch (e) { clog.error("检测屏蔽演员信息, 发生错误:", url, e); show.error("检测屏蔽演员信息, 发生错误:" + e, "bottom", "right"); $checkNewVideoMsg.text(`检测屏蔽演员信息, 发生错误: ${url}`); } } } const CDN_SOURCES = [ { name: "jsDelivr (全球CDN)", json: "https://cdn.jsdelivr.net/gh/gfriends/gfriends/Filetree.json", base: "https://cdn.jsdelivr.net/gh/gfriends/gfriends/Content/" }, { name: "GitHub Raw (备用)", json: "https://raw.githubusercontent.com/gfriends/gfriends/master/Filetree.json", base: "https://raw.githubusercontent.com/gfriends/gfriends/master/Content/" } ]; let currentCdnIndex = parseInt(localStorage.getItem("jhs_img_cdn_index") || "0", 10); (currentCdnIndex >= CDN_SOURCES.length || currentCdnIndex < 0) && (currentCdnIndex = 0); let G_FRIENDS_JSON_URL = CDN_SOURCES[currentCdnIndex].json, CDN_BASE_URL = CDN_SOURCES[currentCdnIndex].base; const STORE_NAME = "filetreeStore", dbHelper = { db: null, async open() { return this.db ? this.db : new Promise(((resolve, reject) => { const request = indexedDB.open("GfriendsAvatarDB", 1); request.onupgradeneeded = event => { this.db = event.target.result; this.db.objectStoreNames.contains(STORE_NAME) || this.db.createObjectStore(STORE_NAME); }; request.onsuccess = event => { this.db = event.target.result; resolve(this.db); }; request.onerror = event => { console.error("IndexedDB open error:", event.target.errorCode); reject(new Error("Failed to open IndexedDB")); }; })); }, async get(key) { await this.open(); return new Promise((resolve => { const request = this.db.transaction([ STORE_NAME ], "readonly").objectStore(STORE_NAME).get(key); request.onsuccess = () => resolve(request.result); request.onerror = () => resolve(null); })); }, async set(key, value) { await this.open(); return new Promise(((resolve, reject) => { const request = this.db.transaction([ STORE_NAME ], "readwrite").objectStore(STORE_NAME).put(value, key); request.onsuccess = () => resolve(); request.onerror = event => { console.error("IndexedDB set error:", event.target.errorCode); reject(new Error("Failed to write to IndexedDB")); }; })); } }; let G_FRIENDS_DATA_CACHE = null, G_FRIENDS_AVATAR_MAP = null; function buildAvatarMap(rawData) { if (!rawData || !rawData.Content) return null; const map = {}, contentData = rawData.Content; for (const companyName in contentData) { const encodedCompany = encodeURIComponent(companyName); for (const fileName in contentData[companyName]) { let cleanNamePart = fileName.replace(/\.jpg$/i, "").split("-")[0]; cleanNamePart.startsWith("AI-Fix-") && (cleanNamePart = cleanNamePart.substring(7)); const actorNameKey = cleanNamePart.toLowerCase().trim(); if (actorNameKey.length > 0) { const actualResourcePath = contentData[companyName][fileName], queryIndex = actualResourcePath.indexOf("?"); let encodedFileNamePart, queryString = ""; if (queryIndex > -1) { encodedFileNamePart = encodeURIComponent(actualResourcePath.substring(0, queryIndex)); queryString = actualResourcePath.substring(queryIndex); } else encodedFileNamePart = encodeURIComponent(actualResourcePath); const fullUrl = `${CDN_BASE_URL}${encodedCompany}/${encodedFileNamePart}${queryString}`; map[actorNameKey] || (map[actorNameKey] = []); map[actorNameKey].includes(fullUrl) || map[actorNameKey].push(fullUrl); } } } return map; } async function searchActorAvatars(searchNames) { let loadObj = loading(); try { await async function() { if (G_FRIENDS_DATA_CACHE && G_FRIENDS_AVATAR_MAP) return G_FRIENDS_DATA_CACHE; let cachedData = null; try { cachedData = await dbHelper.get("filetree_data"); } catch (e) { console.error("读取 IndexedDB 失败:", e); } if (cachedData && cachedData.Content) { G_FRIENDS_DATA_CACHE = cachedData; G_FRIENDS_AVATAR_MAP = buildAvatarMap(cachedData); if (G_FRIENDS_AVATAR_MAP) return G_FRIENDS_DATA_CACHE; } show.info("正在载入头像数据源..."); const response = await fetch(G_FRIENDS_JSON_URL); if (!response.ok) throw new Error(`请求头像源失败: ${response.status}`); const data = await response.json(); if (data && data.Content) { G_FRIENDS_DATA_CACHE = data; G_FRIENDS_AVATAR_MAP = buildAvatarMap(data); try { await dbHelper.set("filetree_data", data); clog.debug("载入头像数据源并写入缓存成功!"); } catch (e) { clog.error(e); show.error("头像数据源写入缓存失败,可能磁盘已满或其他权限问题。"); } return G_FRIENDS_DATA_CACHE; } console.log(data); throw new Error("解析头像数据源失败"); }(); } catch (e) { show.error(e); return []; } finally { loadObj.close(); } if (!G_FRIENDS_AVATAR_MAP) return []; const foundLinks = new Set, searchKeys = searchNames.map((name2 => name2.toLowerCase().trim())).filter((n => n.length > 0)); if (0 === searchKeys.length) return []; for (const searchKey of searchKeys) { const links = G_FRIENDS_AVATAR_MAP[searchKey]; links && links.forEach((link => foundLinks.add(link))); } return Array.from(foundLinks); } class NewVideoPlugin extends BasePlugin { constructor() { super(...arguments); __publicField(this, "currentPage", 1); __publicField(this, "pageSize", 30); } getName() { return "NewVideoPlugin"; } async initCss() { return "\n \n "; } async showNewVideoCount() { const totalNewVideoCount = (await storageManager.getFavoriteActressList()).reduce(((accumulator, actress) => { var _a2; return accumulator + ((null == (_a2 = actress.newVideoList) ? void 0 : _a2.length) ?? 0); }), 0); $("#newVideoCount").text(`${totalNewVideoCount}`); } async resetBtnTip() { const taskPlugin = this.getBean("TaskPlugin"), settingObj = await storageManager.getSetting(), lastCheckTimeStr = localStorage.getItem(taskPlugin.lastCheckFavoriteActressTimeKey) || "无", checkFavoriteActress_IntervalTime = settingObj.checkFavoriteActress_IntervalTime, lastCheckNewVideoTimeStr = localStorage.getItem(taskPlugin.lastCheckNewVideoTimeKey) || "无", checkNewVideo_intervalTime = settingObj.checkNewVideo_intervalTime; $("#checkFavoriteActress").attr("data-tip", `上次自动同步时间: ${lastCheckTimeStr}; 检测间隔时间: ${checkFavoriteActress_IntervalTime}小时`); $("#checkNewVideo").attr("data-tip", `上次检测时间: ${lastCheckNewVideoTimeStr}; 检测间隔时间: ${checkNewVideo_intervalTime}小时`); } async openDialog() { const taskPlugin = this.getBean("TaskPlugin"), settingObj = await storageManager.getSetting(), lastCheckTimeStr = localStorage.getItem(taskPlugin.lastCheckFavoriteActressTimeKey) || "无", checkFavoriteActress_IntervalTime = settingObj.checkFavoriteActress_IntervalTime, lastCheckNewVideoTimeStr = localStorage.getItem(taskPlugin.lastCheckNewVideoTimeKey) || "无", checkNewVideo_intervalTime = settingObj.checkNewVideo_intervalTime; let html = `\n
\n
\n
\n ${this.actressSvg}    手动同步演员\n ${this.newSvg}    手动检测最新作品\n ${this.settingSvg}    配置\n \n
\n
\n \n \n ${this.refreshSvg}    刷新\n
\n\n
\n
\n
\n
\n `; layer.open({ type: 1, title: '新作品检测 ❓', content: html, scrollbar: !1, area: utils.getResponsiveArea([ "80%", "90%" ]), anim: -1, success: async (layero, index) => { this.loadData(); this.bindClick(); utils.setupEscClose(index); } }); } bindClick() { const taskPlugin = this.getBean("TaskPlugin"); $("#reLoad").on("click", (event => { this.loadData(); $("#checkNewVideoMsg").text(""); })); $("#toSetting").on("click", (event => { this.getBean("SettingPlugin").openSettingDialog("task-panel", (() => { $("#setting-checkFavoriteActress").css({ border: "1px solid #f40" }); $("#setting-checkNewVideo").css({ border: "1px solid #f40" }); })); })); $("#checkFavoriteActress").on("click", (event => { utils.q({ clientX: event.clientX, clientY: event.clientY + 20 }, "是否手动同步演员?", (() => { navigator.locks.request(taskPlugin.singleTaskKey, { ifAvailable: !0 }, (async lock => { if (!lock) { show.error("当前有定时任务在后台执行中, 无法发起手动任务"); return; } if ($('a[href*="/users/profile"]').length > 0) { await taskPlugin.checkFavoriteActress(); this.loadData(); } else show.error("未登录JavDb, 同步失败"); })).catch((error => { console.error("锁任务出现错误:", error); clog.error("锁任务出现错误:", error); })); })); })); $("#checkNewVideo").on("click", (event => { utils.q({ clientX: event.clientX, clientY: event.clientY + 20 }, "是否手动检测最新作品?", (() => { navigator.locks.request(taskPlugin.singleTaskKey, { ifAvailable: !0 }, (async lock => { lock ? await taskPlugin.checkNewVideo(!0) : show.error("当前有定时任务在后台执行中, 无法发起手动任务"); })).catch((error => { console.error("锁任务出现错误:", error); clog.error("锁任务出现错误:", error); })); })); })); $("#paramActressType").on("change", (event => { this.loadData(); })); } loadData() { this.currentPage = 1; this.renderActressCards().then(); } async renderActressCards() { const $actressCardContainer = $("#actress-card-container"); if (!$actressCardContainer.length) return; let favoriteActressesList = await storageManager.getFavoriteActressList(); const paramActressType = $("#paramActressType").val(); "all" !== paramActressType && (favoriteActressesList = favoriteActressesList.filter((actress => actress.actressType === paramActressType))); const fullActressList = utils.genericSort(favoriteActressesList, [ { key: item => { var _a2; return (null == (_a2 = item.newVideoList) ? void 0 : _a2.length) ?? 0; }, order: "desc" }, { key: "lastPublishTime", order: "desc" } ]), totalCount = fullActressList.length, totalPages = Math.ceil(totalCount / this.pageSize), startIndex = (this.currentPage - 1) * this.pageSize, endIndex = startIndex + this.pageSize, pagedData = fullActressList.slice(startIndex, endIndex), javDbUrl = await this.getBean("OtherSitePlugin").getJavDbUrl(), taskPlugin = this.getBean("TaskPlugin"), checkNewVideo_ruleTime = await storageManager.getSetting("checkNewVideo_ruleTime") || 8760, cardsHtml = pagedData.map((data => { var _a2, _b, _c; const allNamesStr = Array.isArray(data.allName) ? data.allName.join(",") : ""; Array.isArray(data.newVideoList) && data.newVideoList.join(","); const detailLink = `${javDbUrl}/actors/${data.starId}?t=d`; let isUnCheck = !1; data.lastPublishTime && (isUnCheck = !taskPlugin.isUnnecessaryCheck(data.lastPublishTime, checkNewVideo_ruleTime)); let actressTypeText = "未知", actressTypeBgc = "#9E9E9E"; if ("uncensored" === data.actressType) { actressTypeText = "无码"; actressTypeBgc = "#4CAF50"; } else if ("censored" === data.actressType) { actressTypeText = "有码"; actressTypeBgc = "#FF9800"; } let cardUnnecessaryBtnCss = ""; isUnCheck && (cardUnnecessaryBtnCss = "background: linear-gradient(145deg, #e0e0e0 0%, #cabdbd 100%);box-shadow: none"); return `\n
\n \n ${allNamesStr}\n \n\n
\n \n
${data.name}
\n
\n
${allNamesStr}
\n
\n \n
\n 上次检测: ${data.lastCheckTime || ""}\n
\n
\n 最后发行作品: ${data.lastPublishTime || ""}\n
\n\n
\n ${isUnCheck ? "停更" + checkNewVideo_ruleTime / 24 / 365 + "年以上, 下轮任务不再进行检测" : ""}\n
\n \n
\n ${data.remark || ""}\n
\n \n
\n ${this.editSvg}\n ${this.deleteSvg}\n ${this.checkSvg}\n
\n \n
${actressTypeText}
\n
\n 🔔 ${(null == (_c = data.newVideoList) ? void 0 : _c.length) || 0} \n
\n
\n `; })).join(""); $actressCardContainer.html(cardsHtml); $(".btn-delete-actress").off("click").on("click", (e => { e.preventDefault(); const starId = $(e.currentTarget).attr("data-starId"), actress = fullActressList.find((item => item.starId === starId)); utils.q(e, `是否取消收藏 ${actress.name}?`, (async () => { let deleteActressUrl = `${await this.getBean("OtherSitePlugin").getJavDbUrl()}/actors/${starId}/uncollect`; const csrfToken = document.querySelector("meta[name=csrf-token]").content, res = await gmHttp.post(deleteActressUrl, null, { "x-csrf-token": csrfToken }); if (res.includes("removeClass")) { await storageManager.removeFavoriteActress(starId); this.loadData(); } else { show.error("移除失败"); clog.error("移除失败,返回值:", res); } })); })); $(".btn-edit-actress").off("click").on("click", (e => { e.preventDefault(); const starId = $(e.currentTarget).attr("data-starId"), actress = fullActressList.find((item => item.starId === starId)); actress ? this.editActress(actress) : show.error(`未找到 starId 为 ${starId} 的女优记录。`); })); $(".btn-check-actress").off("click").on("click", (e => { e.preventDefault(); navigator.locks.request(taskPlugin.singleTaskKey, { ifAvailable: !0 }, (async lock => { if (!lock) { show.error("当前有定时任务在后台执行中, 无法发起手动任务"); return; } const starId = $(e.currentTarget).attr("data-starId"), actress = fullActressList.find((item => item.starId === starId)); await taskPlugin.checkOneNewVideo(actress); })).catch((error => { console.error("锁任务出现错误:", error); clog.error("锁任务出现错误:", error); })); })); this.renderPagination(totalCount, totalPages); show.ok("加载完成"); } async editActress(actress) { const initialName = actress.name, initialAvatar = actress.avatar, initialRemark = actress.remark || "", initialAllName = Array.isArray(actress.allName) ? actress.allName.join(",") : "", initialNewVideoList = Array.isArray(actress.newVideoList) ? actress.newVideoList.join(",") : "", starId = actress.starId, textareaStyle = "width: 100%; padding: 8px; border: 1px solid #ccc; border-radius: 4px; min-height: 60px; overflow-y: hidden;", initActressType = actress.actressType || "", editFormHtml = `\n
\n
\n Avatar Preview\n
\n \n \n
\n \n \n
\n
\n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n \n \n
\n
\n `; layer.open({ type: 1, title: `编辑女优: ${initialName} (${starId})`, area: [ "500px", "750px" ], content: editFormHtml, btn: [ "保存", "取消" ], success: (layero, index) => { const autoResizeTextarea = $textarea => { $textarea.css("height", "auto"); $textarea.css("height", $textarea[0].scrollHeight + 15 + "px"); }; $("#edit-actress-avatar").on("input", (function() { const newUrl = $(this).val(); $("#edit-avatar-preview").attr("src", newUrl); })); const $allNameTextarea = $("#edit-actress-allname"); $allNameTextarea.on("input", (function() { autoResizeTextarea($(this)); })); autoResizeTextarea($allNameTextarea); const $videoListTextarea = $("#edit-actress-newvideolist"); $videoListTextarea.on("input", (function() { autoResizeTextarea($(this)); })); autoResizeTextarea($videoListTextarea); $("#search-avatar-btn").on("click", (async () => { await this.searchAvatar(); })); $("#select-cdn-btn").on("click", (async () => { await async function() { const initialIndex = currentCdnIndex, radioOptions = CDN_SOURCES.map(((source, index) => `\n
\n \n \n
\n `)).join(""), cdnSelectHtml = `\n
\n

请选择头像数据源 (当前: ${CDN_SOURCES[initialIndex].name}):

\n ${radioOptions}\n

切换源会清除本地缓存的数据,并在下次搜索时重新加载。

\n
\n `; layer.open({ type: 1, title: "选择 CDN 源", area: [ "400px", "auto" ], content: cdnSelectHtml, btn: [ "确定", "取消" ], success: (layero, index) => { utils.setupEscClose(index); }, yes: async index => { const newIndexStr = $('input[name="cdn-source"]:checked').val(), newIndex = parseInt(newIndexStr, 10); if (newIndex !== currentCdnIndex) { currentCdnIndex = newIndex; localStorage.setItem("jhs_img_cdn_index", newIndex.toString()); G_FRIENDS_JSON_URL = CDN_SOURCES[newIndex].json; CDN_BASE_URL = CDN_SOURCES[newIndex].base; G_FRIENDS_DATA_CACHE = null; G_FRIENDS_AVATAR_MAP = null; try { await dbHelper.set("filetree_data", null); } catch (e) { clog.error("清除 IndexedDB 缓存失败:", e); } show.ok(`CDN 源已切换为: ${CDN_SOURCES[newIndex].name}`); layer.close(index); } else layer.close(index); } }); }(); })); utils.setupEscClose(index); }, yes: async index => { const newAvatar = $("#edit-actress-avatar").val().trim(), newName = $("#edit-actress-name").val().trim(), newAllNameStr = $("#edit-actress-allname").val().trim(), newVideoListStr = $("#edit-actress-newvideolist").val().trim(), newRemark = $("#edit-remark").val().trim(), newActressType = $("#actressType").val(); if (!newName) { show.error("主名称不能为空"); return !1; } const newAllName = newAllNameStr.split(/[\uff0c,]/).map((n => n.trim())).filter((n => n.length > 0)), newVideoList = newVideoListStr.split(/[\uff0c,]/).map((n => n.trim())).filter((n => n.length > 0)); actress.avatar = newAvatar; actress.name = newName; actress.allName = newAllName; actress.newVideoList = newVideoList; actress.actressType = newActressType; actress.remark = newRemark; if (await storageManager.updateFavoriteActress(actress)) show.error("修改失败"); else { show.ok(`女优 ${newName} 信息已更新`); await this.renderActressCards(); layer.close(index); } } }); } renderPagination(totalCount, totalPages) { const currentPage = this.currentPage; let paginationHtml = ""; const $actressPagination = $("#actress-pagination"); if (0 === totalPages) { paginationHtml = '共 0 条记录'; $actressPagination.html(paginationHtml); return; } currentPage > 1 && totalPages > 5 && (paginationHtml += ''); currentPage > 1 && (paginationHtml += ``); let startPage = Math.max(1, currentPage - Math.floor(2.5)), endPage = Math.min(totalPages, startPage + 5 - 1); endPage - startPage < 4 && (startPage = Math.max(1, endPage - 5 + 1)); for (let i = startPage; i <= endPage; i++) { paginationHtml += ``; } currentPage < totalPages && (paginationHtml += ``); currentPage < totalPages && totalPages > 5 && (paginationHtml += ``); paginationHtml += `共 ${totalCount} 条记录 (第 ${currentPage}/${totalPages} 页)`; $actressPagination.html(paginationHtml); $(".pagination-btn").off("click").on("click", (e => { if ($(e.currentTarget).is("[disabled]")) return; const newPage = parseInt($(e.currentTarget).data("page")); if (newPage >= 1 && newPage <= totalPages && newPage !== this.currentPage) { this.currentPage = newPage; this.renderActressCards(); } })); } async searchAvatar() { const $mainNameInput = $("#edit-actress-name"), $aliasInput = $("#edit-actress-allname"), currentName = $mainNameInput.val().trim(), searchNames = $aliasInput.val().trim().split(/[\uff0c,]/).map((n => n.trim())).filter((n => n.length > 0)); currentName && searchNames.unshift(currentName); if (0 === searchNames.length) { show.error("请先填写女优主名称或别名进行搜索。"); return; } const loadObj = loading("正在搜索头像..."); let imageLinks = []; try { imageLinks = await searchActorAvatars(searchNames); } catch (e) { show.error(`头像数据加载或搜索失败: ${e.message || e}`); return; } finally { loadObj.close(); } if (0 === imageLinks.length) { show.error(`未找到与 '${searchNames.join("、")}' 相关的头像。请检查名称。`); return; } const imageItems = imageLinks.map(((url, index) => `\n
\n \n
...
\n
\n `)).join(""), imageListHtml = `\n \n \n
\n

\n 点击图片即可选择(初始共 ${imageLinks.length} 张)\n

\n
\n
\n ${imageItems}\n
\n
\n
\n `; let errorCount = 0; layer.open({ type: 1, title: `选择女优头像 (${imageLinks.length} 张)`, area: utils.getResponsiveArea([ "900px", "85%" ]), content: imageListHtml, btn: [ "关闭" ], success: (selectionLayero, selectionIndex) => { const $container = $(selectionLayero), $images = $container.find(".gfriends-selectable-img"), $prompt = $container.find("#gfriends-prompt"); $images.each((function() { const $img = $(this), wrapperId = $img.data("wrapper-id"), $wrapper = $container.find(`#${wrapperId}`), $sizeTag = $container.find(`.gfriends-size-tag[data-size-for="${wrapperId}"]`); $img.on("load", (function() { const width = this.naturalWidth, height = this.naturalHeight; $sizeTag.text(`${width} x ${height}`); })); $img.on("error", (function() { $wrapper.remove(); errorCount++; const validCount = imageLinks.length - errorCount; $prompt.text(`点击图片即可选择(已移除 ${errorCount} 张错误图片,剩余 ${validCount} 张)`); if (0 === validCount) { show.error("所有搜索到的头像链接均已失效,无法选择。"); layer.close(selectionIndex); } })); this.complete && (this.naturalWidth > 0 ? $img.trigger("load") : $img.trigger("error")); })); $images.on("click", (function() { const $clickedImg = $(this), selectedUrl = $clickedImg.data("url"); $("#edit-actress-avatar").val(selectedUrl); $("#edit-avatar-preview").attr("src", selectedUrl); $images.removeClass("is-selected"); $clickedImg.addClass("is-selected"); setTimeout((() => { layer.close(selectionIndex); }), 150); })); utils.setupEscClose(selectionIndex); } }); } } const originalLayerClose = layer.close; layer.close = function(index) { const result = originalLayerClose.call(this, index); !function(waitTime = 10) { setTimeout((() => { const openLayerCount = document.querySelectorAll(".layui-layer-shade").length; document.documentElement.style.overflow = openLayerCount > 0 ? "hidden" : ""; }), waitTime); }(); return result; }; const originalLayerOpen = layer.open; layer.open = function(options) { const originalSuccess = (options = options || {}).success; options.success = function(layero, index) { "function" == typeof originalSuccess && originalSuccess.call(this, layero, index); utils.setupEscClose(index); }; return originalLayerOpen.call(this, options); }; utils.importResource("https://cdn.jsdelivr.net/npm/layui-layer@1.0.9/layer.min.css"); utils.importResource("https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.css"); utils.importResource("https://cdn.jsdelivr.net/npm/viewerjs@1.11.1/dist/viewer.min.css"); utils.importResource("https://cdn.jsdelivr.net/npm/tabulator-tables@6.3.1/dist/css/tabulator_semanticui.min.css"); const pluginManager = function() { const pluginManager2 = new PluginManager; unsafeWindow.pluginManager = window.pluginManager = pluginManager2; let hostname = window.location.hostname; if (isJavDb) { pluginManager2.register(ListPagePlugin); pluginManager2.register(AutoPagePlugin); pluginManager2.register(Fc2Plugin); pluginManager2.register(FoldCategoryPlugin); pluginManager2.register(ListPageButtonPlugin); pluginManager2.register(HistoryPlugin); pluginManager2.register(SettingPlugin); pluginManager2.register(NavBarPlugin); pluginManager2.register(HitShowPlugin); pluginManager2.register(TOP250Plugin); pluginManager2.register(CoverButtonPlugin); pluginManager2.register(ImageRecognitionPlugin); pluginManager2.register(Fc2By123AvPlugin); pluginManager2.register(WangPan115MatchPlugin); pluginManager2.register(DetailPagePlugin); pluginManager2.register(ReviewPlugin); pluginManager2.register(RelatedPlugin); pluginManager2.register(DetailPageButtonPlugin); pluginManager2.register(HighlightMagnetPlugin); pluginManager2.register(PreviewVideoPlugin); pluginManager2.register(FilterTitleKeywordPlugin); pluginManager2.register(ActressInfoPlugin); pluginManager2.register(OtherSitePlugin); pluginManager2.register(WangPan115TaskPlugin); pluginManager2.register(TranslatePlugin); pluginManager2.register(WantAndWatchedVideosPlugin); pluginManager2.register(MagnetHubPlugin); pluginManager2.register(ScreenShotPlugin); pluginManager2.register(BlacklistPlugin); pluginManager2.register(FavoriteActressesPlugin); pluginManager2.register(NewVideoPlugin); pluginManager2.register(TaskPlugin); } if (isJavBus) { pluginManager2.register(ListPagePlugin); pluginManager2.register(ListPageButtonPlugin); pluginManager2.register(SettingPlugin); pluginManager2.register(HistoryPlugin); pluginManager2.register(AutoPagePlugin); pluginManager2.register(ImageRecognitionPlugin); pluginManager2.register(BusNavBarPlugin); pluginManager2.register(CoverButtonPlugin); pluginManager2.register(WangPan115MatchPlugin); pluginManager2.register(BusImgPlugin); pluginManager2.register(BusDetailPagePlugin); pluginManager2.register(DetailPageButtonPlugin); pluginManager2.register(ReviewPlugin); pluginManager2.register(FilterTitleKeywordPlugin); pluginManager2.register(HighlightMagnetPlugin); pluginManager2.register(BusPreviewVideoPlugin); pluginManager2.register(MagnetHubPlugin); pluginManager2.register(ScreenShotPlugin); pluginManager2.register(OtherSitePlugin); pluginManager2.register(WangPan115TaskPlugin); pluginManager2.register(TranslatePlugin); pluginManager2.register(BlacklistPlugin); pluginManager2.register(TaskPlugin); } hostname.includes("javtrailers") && pluginManager2.register(JavTrailersPlugin); hostname.includes("subtitlecat") && pluginManager2.register(SubTitleCatPlugin); (hostname.includes("aliyundrive") || hostname.includes("alipan")) && pluginManager2.register(AliyunPanPlugin); hostname.includes("115.com") && pluginManager2.register(WangPan115Plugin); return pluginManager2; }(); pluginManager.processCss().then(); !async function() { window.isDetailPage = function() { let href = window.location.href; return isJavDb ? href.split("?")[0].includes("/v/") : !!isJavBus && $("#magnet-table").length > 0; }(); window.isListPage = function() { let href = window.location.href; return isJavDb ? $(".movie-list").length > 0 || href.includes("advanced_search") : !!isJavBus && $(".masonry > div .item").length > 0; }(); window.isFc2Page = function() { let href = window.location.href; return href.includes("advanced_search?type=3") || href.includes("advanced_search?type=100"); }(); isJavDb && /(^|;)\s*locale\s*=\s*en\s*($|;)/i.test(document.cookie) && show.error("请切换到中文语言下才可正常使用本脚本", { duration: -1 }); pluginManager.processPlugins().then(); }();