diff --git a/dist/jhs-enhance.user.js b/dist/jhs-enhance.user.js index 0bc3687..b756435 100644 --- a/dist/jhs-enhance.user.js +++ b/dist/jhs-enhance.user.js @@ -76,17 +76,17 @@ (function ($$1, localforage$1, Toastify$1, md5$1, Viewer$1, QRCode$1) { 'use strict'; - var __defProp = Object.defineProperty; - var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { - enumerable: true, - configurable: true, - writable: true, - value - }) : obj[key] = value; - var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); - var __defProp2 = Object.defineProperty, __typeError = (msg) => { + var __defProp = Object.defineProperty; + var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { + enumerable: true, + configurable: true, + writable: true, + value + }) : obj[key] = value; + var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value); + var __defProp2 = Object.defineProperty, __typeError = (msg) => { throw TypeError(msg); - }, __publicField2 = (obj, key, value) => ((obj2, key2, value2) => key2 in obj2 ? __defProp2(obj2, key2, { + }, __publicField2 = (obj, key, value) => ((obj2, key2, value2) => key2 in obj2 ? __defProp2(obj2, key2, { enumerable: true, configurable: true, writable: true, @@ -825,7 +825,7 @@ } `; - const subtitleModalCss = ` + const subtitleModalCss = ` \n" ); insertStyle(mainCss); - insertStyle(subtitleModalCss); + insertStyle(subtitleModalCss); window.$ = window.jQuery = $$1; - if (typeof $$1.trim !== "function") { - $$1.trim = function (text) { - return text == null ? "" : (text + "").trim(); - }; - } + if (typeof $$1.trim !== "function") { + $$1.trim = function (text) { + return text == null ? "" : (text + "").trim(); + }; + } window.localforage = localforage$1; window.Toastify = Toastify$1; window.md5 = md5$1; @@ -1125,131 +1125,131 @@ window.Status_HAS_WATCH = Status_HAS_WATCH$1; window.NO = NO; window.YES = YES; - const HAS_GM = typeof GM !== "undefined"; - const NEW_GM = ((scope, GM2) => { - if (typeof GM_info === "undefined" || GM_info.scriptHandler !== "Tampermonkey" || compareVersions(GM_info.version, "5.3.2") < 0) return; - const GM_xmlhttpRequestOrig = GM_xmlhttpRequest; - const GM_xmlHttpRequestOrig = GM2.xmlHttpRequest; + const HAS_GM = typeof GM !== "undefined"; + const NEW_GM = ((scope, GM2) => { + if (typeof GM_info === "undefined" || GM_info.scriptHandler !== "Tampermonkey" || compareVersions(GM_info.version, "5.3.2") < 0) return; + const GM_xmlhttpRequestOrig = GM_xmlhttpRequest; + const GM_xmlHttpRequestOrig = GM2.xmlHttpRequest; - function compareVersions(v1, v2) { - const parts1 = v1.split(".").map(Number); - const parts2 = v2.split(".").map(Number); - const length = Math.max(parts1.length, parts2.length); - for (let i = 0; i < length; i++) { - const num1 = parts1[i] || 0; - const num2 = parts2[i] || 0; - if (num1 > num2) return 1; - if (num1 < num2) return -1; + function compareVersions(v1, v2) { + const parts1 = v1.split(".").map(Number); + const parts2 = v2.split(".").map(Number); + const length = Math.max(parts1.length, parts2.length); + for (let i = 0; i < length; i++) { + const num1 = parts1[i] || 0; + const num2 = parts2[i] || 0; + if (num1 > num2) return 1; + if (num1 < num2) return -1; + } + return 0; + } + + function GM_xmlhttpRequestWrapper(odetails) { + if (odetails.redirect !== void 0) { + return GM_xmlhttpRequestOrig(odetails); + } + if (odetails.onprogress || odetails.fetch === false) { + console.warn("Fetch mode does not support onprogress in the background."); + } + const { + onload, + onloadend, + onerror, + onabort, + ontimeout, + ...details + } = odetails; + const handleRedirects = (initialDetails) => { + const request = GM_xmlhttpRequestOrig({ + ...initialDetails, + redirect: "manual", + onload: function (response) { + if (response.status >= 300 && response.status < 400) { + const m = response.responseHeaders.match(/Location:\s*(\S+)/i); + const redirectUrl = m && m[1]; + if (redirectUrl) { + const absoluteUrl = new URL(redirectUrl, initialDetails.url).href; + handleRedirects({...initialDetails, url: absoluteUrl}); + return; + } } - return 0; - } + if (onload) onload.call(this, response); + if (onloadend) onloadend.call(this, response); + }, + onerror: function (response) { + if (onerror) onerror.call(this, response); + if (onloadend) onloadend.call(this, response); + }, + onabort: function (response) { + if (onabort) onabort.call(this, response); + if (onloadend) onloadend.call(this, response); + }, + ontimeout: function (response) { + if (ontimeout) ontimeout.call(this, response); + if (onloadend) onloadend.call(this, response); + } + }); + return request; + }; + return handleRedirects(details); + } - function GM_xmlhttpRequestWrapper(odetails) { - if (odetails.redirect !== void 0) { - return GM_xmlhttpRequestOrig(odetails); - } - if (odetails.onprogress || odetails.fetch === false) { - console.warn("Fetch mode does not support onprogress in the background."); - } - const { - onload, - onloadend, - onerror, - onabort, - ontimeout, - ...details - } = odetails; - const handleRedirects = (initialDetails) => { - const request = GM_xmlhttpRequestOrig({ - ...initialDetails, - redirect: "manual", - onload: function (response) { - if (response.status >= 300 && response.status < 400) { - const m = response.responseHeaders.match(/Location:\s*(\S+)/i); - const redirectUrl = m && m[1]; - if (redirectUrl) { - const absoluteUrl = new URL(redirectUrl, initialDetails.url).href; - handleRedirects({...initialDetails, url: absoluteUrl}); - return; - } - } - if (onload) onload.call(this, response); - if (onloadend) onloadend.call(this, response); - }, - onerror: function (response) { - if (onerror) onerror.call(this, response); - if (onloadend) onloadend.call(this, response); - }, - onabort: function (response) { - if (onabort) onabort.call(this, response); - if (onloadend) onloadend.call(this, response); - }, - ontimeout: function (response) { - if (ontimeout) ontimeout.call(this, response); - if (onloadend) onloadend.call(this, response); - } - }); - return request; - }; - return handleRedirects(details); - } - - function GM_xmlHttpRequestWrapper(odetails) { - let abort; - const p = new Promise((resolve, reject) => { - const {onload, ontimeout, onerror, ...send} = odetails; - send.onerror = function (r) { - if (onerror) { - resolve(r); - onerror.call(this, r); - } else { - reject(r); - } - }; - send.ontimeout = function (r) { - if (ontimeout) { - resolve(r); - ontimeout.call(this, r); - } else { - reject(r); - } - }; - send.onload = function (r) { - resolve(r); - if (onload) onload.call(this, r); - }; - const a = GM_xmlhttpRequestWrapper(send).abort; - if (abort === true) { - a(); - } else { - abort = a; - } - }); - p.abort = () => { - if (typeof abort === "function") { - abort(); - } else { - abort = true; - } - }; - return p; - } - - GM_xmlhttpRequest = GM_xmlhttpRequestWrapper; - scope.GM_xmlhttpRequestOrig = GM_xmlhttpRequestOrig; - const gopd = Object.getOwnPropertyDescriptor(GM2, "xmlHttpRequest"); - if (gopd && gopd.configurable === false) { - return { - __proto__: GM2, - xmlHttpRequest: GM_xmlHttpRequestWrapper, - xmlHttpRequestOrig: GM_xmlHttpRequestOrig - }; + function GM_xmlHttpRequestWrapper(odetails) { + let abort; + const p = new Promise((resolve, reject) => { + const {onload, ontimeout, onerror, ...send} = odetails; + send.onerror = function (r) { + if (onerror) { + resolve(r); + onerror.call(this, r); + } else { + reject(r); + } + }; + send.ontimeout = function (r) { + if (ontimeout) { + resolve(r); + ontimeout.call(this, r); + } else { + reject(r); + } + }; + send.onload = function (r) { + resolve(r); + if (onload) onload.call(this, r); + }; + const a = GM_xmlhttpRequestWrapper(send).abort; + if (abort === true) { + a(); } else { - GM2.xmlHttpRequest = GM_xmlHttpRequestWrapper; - GM2.xmlHttpRequestOrig = GM_xmlHttpRequestOrig; + abort = a; } - })(globalThis, HAS_GM ? GM : {}); - if (HAS_GM && NEW_GM) GM = NEW_GM; + }); + p.abort = () => { + if (typeof abort === "function") { + abort(); + } else { + abort = true; + } + }; + return p; + } + + GM_xmlhttpRequest = GM_xmlhttpRequestWrapper; + scope.GM_xmlhttpRequestOrig = GM_xmlhttpRequestOrig; + const gopd = Object.getOwnPropertyDescriptor(GM2, "xmlHttpRequest"); + if (gopd && gopd.configurable === false) { + return { + __proto__: GM2, + xmlHttpRequest: GM_xmlHttpRequestWrapper, + xmlHttpRequestOrig: GM_xmlHttpRequestOrig + }; + } else { + GM2.xmlHttpRequest = GM_xmlHttpRequestWrapper; + GM2.xmlHttpRequestOrig = GM_xmlHttpRequestOrig; + } + })(globalThis, HAS_GM ? GM : {}); + if (HAS_GM && NEW_GM) GM = NEW_GM; var _StorageManager_instances, setItem_fn, saveFilterItem_fn; _StorageManager_instances = /* @__PURE__ */ new WeakSet(); setItem_fn = async function(key, data) { @@ -1278,24 +1278,24 @@ 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; - __publicField2(this, "car_list_key", "car_list"); - __publicField2(this, "filter_keyword_title_key", "filter_keyword_title"); - __publicField2(this, "filter_keyword_review_key", "filter_keyword_review"); - __publicField2(this, "setting_key", "setting"); - __publicField2(this, "blacklist_key", "blacklist"); - __publicField2(this, "blacklist_car_list_key", "blacklist_car_list"); - __publicField2(this, "favorite_actresses_key", "favorite_actresses"); - __publicField2(this, "highlighted_tags_key", "highlighted_tags"); - __publicField2(this, "forage", localforage.createInstance({ + __publicField2(this, "car_list_key", "car_list"); + __publicField2(this, "filter_keyword_title_key", "filter_keyword_title"); + __publicField2(this, "filter_keyword_review_key", "filter_keyword_review"); + __publicField2(this, "setting_key", "setting"); + __publicField2(this, "blacklist_key", "blacklist"); + __publicField2(this, "blacklist_car_list_key", "blacklist_car_list"); + __publicField2(this, "favorite_actresses_key", "favorite_actresses"); + __publicField2(this, "highlighted_tags_key", "highlighted_tags"); + __publicField2(this, "forage", localforage.createInstance({ driver: localforage.INDEXEDDB, name: "JAV-JHS", version: 1, storeName: "appData" })); - __publicField2(this, "cacheCarList", null); - __publicField2(this, "cacheBlacklistCarList", null); - __publicField2(this, "cacheFavoriteActressList", null); - __publicField2(this, "cacheSettingObj", null); + __publicField2(this, "cacheCarList", null); + __publicField2(this, "cacheBlacklistCarList", null); + __publicField2(this, "cacheFavoriteActressList", null); + __publicField2(this, "cacheSettingObj", null); if (_StorageManager.instance) throw new Error("StorageManager已被实例化过了!"); _StorageManager.instance = this; } @@ -1856,8 +1856,8 @@ var _GM_xmlhttpRequest = /* @__PURE__ */ (() => typeof GM_xmlhttpRequest != "undefined" ? GM_xmlhttpRequest : void 0)(); class Utils { constructor() { - __publicField2(this, "intervalContainer", {}); - __publicField2(this, "mimeTypes", { + __publicField2(this, "intervalContainer", {}); + __publicField2(this, "mimeTypes", { txt: "text/plain", html: "text/html", css: "text/css", @@ -1886,14 +1886,14 @@ webm: "video/webm", ogg: "audio/ogg" }); - __publicField2(this, "timers", /* @__PURE__ */ new Map()); - __publicField2(this, "insertStyle", ((css) => { + __publicField2(this, "timers", /* @__PURE__ */ new Map()); + __publicField2(this, "insertStyle", ((css) => { if (css) { -1 === css.indexOf(""); $("head").append(css); } })); - __publicField2(this, "layerIndexStack", []); + __publicField2(this, "layerIndexStack", []); Utils.instance || (Utils.instance = this); return Utils.instance; } @@ -2498,26 +2498,26 @@ ${value}\r } class BasePlugin { constructor() { - __publicField2(this, "pluginManager", null); - __publicField2(this, "settingSvg", ''); - __publicField2(this, "editSvg", ''); - __publicField2(this, "deleteSvg", ''); - __publicField2(this, "checkSvg", ''); - __publicField2(this, "actressSvg", ''); - __publicField2(this, "newSvg", ''); - __publicField2(this, "refreshSvg", ''); - __publicField2(this, "blacklistSvg", ''); - __publicField2(this, "removeSvg", ''); - __publicField2(this, "copySvg", ''); - __publicField2(this, "titleSvg", ''); - __publicField2(this, "carNumSvg", ''); - __publicField2(this, "downSvg", ''); - __publicField2(this, "handleSvg", ''); - __publicField2(this, "siteSvg", ''); - __publicField2(this, "videoSvg", ''); - __publicField2(this, "screenSvg", ''); - __publicField2(this, "recoveryVideoSvg", ''); - __publicField2(this, "markDeleteSvg", ''); + __publicField2(this, "pluginManager", null); + __publicField2(this, "settingSvg", ''); + __publicField2(this, "editSvg", ''); + __publicField2(this, "deleteSvg", ''); + __publicField2(this, "checkSvg", ''); + __publicField2(this, "actressSvg", ''); + __publicField2(this, "newSvg", ''); + __publicField2(this, "refreshSvg", ''); + __publicField2(this, "blacklistSvg", ''); + __publicField2(this, "removeSvg", ''); + __publicField2(this, "copySvg", ''); + __publicField2(this, "titleSvg", ''); + __publicField2(this, "carNumSvg", ''); + __publicField2(this, "downSvg", ''); + __publicField2(this, "handleSvg", ''); + __publicField2(this, "siteSvg", ''); + __publicField2(this, "videoSvg", ''); + __publicField2(this, "screenSvg", ''); + __publicField2(this, "recoveryVideoSvg", ''); + __publicField2(this, "markDeleteSvg", ''); } getName() { throw new Error(`${this.constructor.name} 未显示getName()`); @@ -2693,374 +2693,374 @@ ${value}\r } } - const DMM_GRAPHQL_URL = "https://api.video.dmm.co.jp/graphql"; - const dmmGraphQLKeyword = (code) => { - const match = String(code || "").trim().toUpperCase().match(/^([A-Z0-9]{2,10})-(\d{1,6})$/); - if (!match) return ""; - return `${match[1].toLowerCase()}${match[2].padStart(5, "0")}`; - }; - const dmmGraphQLRequest = (query) => { - return new Promise((resolve, reject) => { - _GM_xmlhttpRequest({ - method: "POST", - url: DMM_GRAPHQL_URL, - data: JSON.stringify({query}), - timeout: 12e3, - headers: { - Accept: "application/json", - "Content-Type": "application/json", - Referer: "https://video.dmm.co.jp/", - "Fanza-Device": "BROWSER", - "Cache-Control": "no-cache" - }, - onload: (response) => { - if (response.status < 200 || response.status >= 400) { - reject(new Error(`DMM GraphQL HTTP ${response.status || 0}`)); - return; - } - try { - const json = JSON.parse(response.responseText || "{}"); - if (Array.isArray(json.errors) && json.errors.length) { - reject(new Error(json.errors[0].message || "DMM GraphQL error")); - return; - } - resolve(json.data || {}); - } catch (e) { - reject(new Error(`DMM GraphQL JSON parse error: ${e.message}`)); - } - }, - onerror: () => reject(new Error("DMM GraphQL network error")), - ontimeout: () => reject(new Error("DMM GraphQL timeout")) - }); - }); - }; - const searchDmmContentIdsByGraphQL = async (code) => { - const keyword = dmmGraphQLKeyword(code); - if (!keyword) return []; - const carNumNoHyphen = String(code || "").replace(/-/g, "").toLowerCase(); - const results = []; - const searchKeywords = [.../* @__PURE__ */ new Set([keyword, `${keyword}#`])]; - for (const searchKeyword of searchKeywords) { - const query = `{ legacySearchPPV(limit: 10, offset: 0, sort: SALES_RANK_SCORE, floor: AV, queryWord: ${JSON.stringify(searchKeyword)}) { result { contents { contentId serviceCode floorCode } } } }`; - const data = await dmmGraphQLRequest(query); - const contents = Array.isArray(data && data.legacySearchPPV && data.legacySearchPPV.result && data.legacySearchPPV.result.contents) ? data.legacySearchPPV.result.contents : []; - for (const item of contents) { - const contentId = String(item.contentId || ""); - if (contentId.replace(/[^a-z0-9]/gi, "").toLowerCase().includes(carNumNoHyphen)) { - results.push({ - serviceCode: item.serviceCode || "digital", - floorCode: item.floorCode || "AV", - contentId - }); - } + const DMM_GRAPHQL_URL = "https://api.video.dmm.co.jp/graphql"; + const dmmGraphQLKeyword = (code) => { + const match = String(code || "").trim().toUpperCase().match(/^([A-Z0-9]{2,10})-(\d{1,6})$/); + if (!match) return ""; + return `${match[1].toLowerCase()}${match[2].padStart(5, "0")}`; + }; + const dmmGraphQLRequest = (query) => { + return new Promise((resolve, reject) => { + _GM_xmlhttpRequest({ + method: "POST", + url: DMM_GRAPHQL_URL, + data: JSON.stringify({query}), + timeout: 12e3, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + Referer: "https://video.dmm.co.jp/", + "Fanza-Device": "BROWSER", + "Cache-Control": "no-cache" + }, + onload: (response) => { + if (response.status < 200 || response.status >= 400) { + reject(new Error(`DMM GraphQL HTTP ${response.status || 0}`)); + return; + } + try { + const json = JSON.parse(response.responseText || "{}"); + if (Array.isArray(json.errors) && json.errors.length) { + reject(new Error(json.errors[0].message || "DMM GraphQL error")); + return; } - if (results.length > 0) break; + resolve(json.data || {}); + } catch (e) { + reject(new Error(`DMM GraphQL JSON parse error: ${e.message}`)); + } + }, + onerror: () => reject(new Error("DMM GraphQL network error")), + ontimeout: () => reject(new Error("DMM GraphQL timeout")) + }); + }); + }; + const searchDmmContentIdsByGraphQL = async (code) => { + const keyword = dmmGraphQLKeyword(code); + if (!keyword) return []; + const carNumNoHyphen = String(code || "").replace(/-/g, "").toLowerCase(); + const results = []; + const searchKeywords = [.../* @__PURE__ */ new Set([keyword, `${keyword}#`])]; + for (const searchKeyword of searchKeywords) { + const query = `{ legacySearchPPV(limit: 10, offset: 0, sort: SALES_RANK_SCORE, floor: AV, queryWord: ${JSON.stringify(searchKeyword)}) { result { contents { contentId serviceCode floorCode } } } }`; + const data = await dmmGraphQLRequest(query); + const contents = Array.isArray(data && data.legacySearchPPV && data.legacySearchPPV.result && data.legacySearchPPV.result.contents) ? data.legacySearchPPV.result.contents : []; + for (const item of contents) { + const contentId = String(item.contentId || ""); + if (contentId.replace(/[^a-z0-9]/gi, "").toLowerCase().includes(carNumNoHyphen)) { + results.push({ + serviceCode: item.serviceCode || "digital", + floorCode: item.floorCode || "AV", + contentId + }); } - return results; - }; - const getDmmGraphQLVideoUrls = async (code) => { - const contentItems = await searchDmmContentIdsByGraphQL(code); - if (!contentItems.length) return null; - const results = await Promise.allSettled( - contentItems.map((item) => extractTrailerLinks(item)) - ); - for (const result of results) { - if (result.status === "fulfilled" && result.value) { - return result.value; + } + if (results.length > 0) break; + } + return results; + }; + const getDmmGraphQLVideoUrls = async (code) => { + const contentItems = await searchDmmContentIdsByGraphQL(code); + if (!contentItems.length) return null; + const results = await Promise.allSettled( + contentItems.map((item) => extractTrailerLinks(item)) + ); + for (const result of results) { + if (result.status === "fulfilled" && result.value) { + return result.value; + } + } + return null; + }; + const extractTrailerLinks = (item) => { + const {contentId, serviceCode, floorCode} = item; + return new Promise((resolve, reject) => { + const trailerPageUrl = `https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=${contentId}/mtype=AhRVShI_/service=${serviceCode}/floor=${floorCode}/mode=/`; + _GM_xmlhttpRequest({ + method: "GET", + url: trailerPageUrl, + timeout: 15e3, + headers: { + "accept-language": "ja-JP,ja;q=0.9", + Cookie: "age_check_done=1" + }, + onload: (response) => { + const htmlContent = response.responseText || ""; + if (htmlContent.includes("このサービスはお住まいの地域からは")) { + reject(new Error("节点不可用,请将DMM域名分流到日本ip")); + return; + } + const match = htmlContent.match(/const\s+args\s+=\s+(.*);/); + if (!match) { + reject(new Error("未在脚本中找到 const args = ... 变量")); + return; + } + let bitrates; + try { + bitrates = JSON.parse(match[1]).bitrates; + } catch (e) { + reject(new Error(`解析播放器脚本 JSON 失败: ${e.message}`)); + return; + } + if (!Array.isArray(bitrates)) { + reject(new Error("解析画质链接失败: bitrates 字段不是一个数组或不存在")); + return; + } + const qualityKeys = [ + "4ks", + "4k", + "hhbs", + "hhb", + "hmb", + "mhb", + "mmb", + "dmb_w", + "dmb_s", + "dm_s", + "sm_s", + "mhb_w" + ]; + const qualityNameRegex = new RegExp(`(${qualityKeys.join("|")})\\.mp4$`); + const finalQualityMap = {}; + for (const bitrate of bitrates) { + const url = bitrate && bitrate.src; + if (!url || typeof url !== "string" || !url.endsWith(".mp4")) continue; + const qualityMatch = url.match(qualityNameRegex); + let qualityKey = ""; + if (qualityMatch && qualityMatch[1]) qualityKey = qualityMatch[1]; + if (qualityKey && !finalQualityMap[qualityKey]) { + finalQualityMap[qualityKey] = url; } - } - return null; - }; - const extractTrailerLinks = (item) => { - const {contentId, serviceCode, floorCode} = item; - return new Promise((resolve, reject) => { - const trailerPageUrl = `https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=${contentId}/mtype=AhRVShI_/service=${serviceCode}/floor=${floorCode}/mode=/`; - _GM_xmlhttpRequest({ - method: "GET", - url: trailerPageUrl, - timeout: 15e3, - headers: { - "accept-language": "ja-JP,ja;q=0.9", - Cookie: "age_check_done=1" - }, - onload: (response) => { - const htmlContent = response.responseText || ""; - if (htmlContent.includes("このサービスはお住まいの地域からは")) { - reject(new Error("节点不可用,请将DMM域名分流到日本ip")); - return; - } - const match = htmlContent.match(/const\s+args\s+=\s+(.*);/); - if (!match) { - reject(new Error("未在脚本中找到 const args = ... 变量")); - return; - } - let bitrates; - try { - bitrates = JSON.parse(match[1]).bitrates; - } catch (e) { - reject(new Error(`解析播放器脚本 JSON 失败: ${e.message}`)); - return; - } - if (!Array.isArray(bitrates)) { - reject(new Error("解析画质链接失败: bitrates 字段不是一个数组或不存在")); - return; - } - const qualityKeys = [ - "4ks", - "4k", - "hhbs", - "hhb", - "hmb", - "mhb", - "mmb", - "dmb_w", - "dmb_s", - "dm_s", - "sm_s", - "mhb_w" - ]; - const qualityNameRegex = new RegExp(`(${qualityKeys.join("|")})\\.mp4$`); - const finalQualityMap = {}; - for (const bitrate of bitrates) { - const url = bitrate && bitrate.src; - if (!url || typeof url !== "string" || !url.endsWith(".mp4")) continue; - const qualityMatch = url.match(qualityNameRegex); - let qualityKey = ""; - if (qualityMatch && qualityMatch[1]) qualityKey = qualityMatch[1]; - if (qualityKey && !finalQualityMap[qualityKey]) { - finalQualityMap[qualityKey] = url; - } - } - if (Object.keys(finalQualityMap).length === 0) { - reject(new Error("未找到匹配要求的预览画质视频")); - return; - } - resolve(finalQualityMap); - }, - onerror: () => reject(new Error("DMM 播放页请求网络错误")), - ontimeout: () => reject(new Error("DMM 播放页请求超时")) - }); - }); - }; - const selectDefaultQuality = (dmmVideoQualityList, intendedDefault) => { - if (!dmmVideoQualityList || 0 === dmmVideoQualityList.length) return null; - const availableSet = new Set(dmmVideoQualityList); - if (availableSet.has(intendedDefault)) return intendedDefault; - const priorityOrder = qualityOptions.map(((option) => option.quality)).reverse(); - for (const quality of priorityOrder) if (availableSet.has(quality)) return quality; - return dmmVideoQualityList[0]; - }, CACHE_KEY = "jhs_dmm_video"; + } + if (Object.keys(finalQualityMap).length === 0) { + reject(new Error("未找到匹配要求的预览画质视频")); + return; + } + resolve(finalQualityMap); + }, + onerror: () => reject(new Error("DMM 播放页请求网络错误")), + ontimeout: () => reject(new Error("DMM 播放页请求超时")) + }); + }); + }; + const selectDefaultQuality = (dmmVideoQualityList, intendedDefault) => { + if (!dmmVideoQualityList || 0 === dmmVideoQualityList.length) return null; + const availableSet = new Set(dmmVideoQualityList); + if (availableSet.has(intendedDefault)) return intendedDefault; + const priorityOrder = qualityOptions.map(((option) => option.quality)).reverse(); + for (const quality of priorityOrder) if (availableSet.has(quality)) return quality; + return dmmVideoQualityList[0]; + }, CACHE_KEY = "jhs_dmm_video"; - class DmmVideoFetcher { - constructor(carNum2, showErrorMessages = true) { - this.carNum = carNum2; - this.showErrorMessages = showErrorMessages; - } - - _checkCache() { - const cachedData = localStorage.getItem(CACHE_KEY) ? JSON.parse(localStorage.getItem(CACHE_KEY)) : {}; - if (cachedData[this.carNum]) { - clog.debug("缓存中存在预览视频信息", cachedData[this.carNum]); - return cachedData[this.carNum]; - } - return null; - } - - _updateCache(videoMap) { - const cachedData = localStorage.getItem(CACHE_KEY) ? JSON.parse(localStorage.getItem(CACHE_KEY)) : {}; - cachedData[this.carNum] = videoMap; - clog.debug("成功解析出预览视频并已缓存:", videoMap); - localStorage.setItem(CACHE_KEY, JSON.stringify(cachedData)); - } - - async _searchContentIds() { - const carNum2 = this.carNum, carNumNoHyphen = carNum2.replace(/-/g, ""), keywordAttempts = [{ - keyword: carNum2.replace("-", "00"), - name: "00-替换关键词" - }, { - keyword: carNum2, - name: "原始番号关键词" - }, { - keyword: carNumNoHyphen, - name: "无连字符关键词" - }], carNumLower = carNum2.toLowerCase(); - for (const attempt of keywordAttempts) { - const {keyword, name: name2} = attempt, currentTempCarNumLower = keyword.toLowerCase(), - apiUrl2 = `https://api.dmm.com/affiliate/v3/ItemList?${new URLSearchParams({ - api_id: "UrwskPfkqQ0DuVry2gYL", - affiliate_id: "10278-996", - output: "json", - site: "FANZA", - sort: "match", - keyword - }).toString()}`; - let response; - try { - response = await gmHttp.get(apiUrl2); - } catch (e) { - clog.error(`API 请求失败,跳过 ${name2}:`, e); - continue; - } - if (!response || !response.result || !response.result.result_count) { - clog.debug(`使用 ${name2} (${keyword}) 进行 API 搜索 返回无结果,尝试下一个关键词。`); - continue; - } - const newItems = []; - for (const item of response.result.items) { - if (newItems.length >= 2) break; - const contentId = item.content_id || "", makerProduct = item.maker_product || ""; - if (contentId.includes(currentTempCarNumLower.replace("-", "")) || carNumLower === makerProduct.toLowerCase() || contentId.includes(carNumNoHyphen.toLowerCase())) { - newItems.push({ - serviceCode: item.service_code, - floorCode: item.floor_code, - contentId, - pageUrl: item.URL - }); - clog.debug(`[${name2}] cid|makerProduct 匹配成功:`, contentId, makerProduct); - } - } - if (newItems.length > 0) { - clog.debug(`--- 成功通过 ${name2} 找到 Content IDs ---`); - const $btn2 = $("#fanzaBtn"); - let url = `https://www.dmm.co.jp/search/=/searchstr=${keyword}`, type = "single"; - if (newItems.length > 1) { - $btn2.attr("href", url); - $btn2.append('多结果'); - $btn2.css("backgroundColor", "#7bc73b"); - type = "multiple"; - } else { - url = newItems[0].pageUrl; - $btn2.attr("href", url); - $btn2.css("backgroundColor", "#7bc73b"); - } - const dmmCacheKey = "jhs_other_site_dmm", - dmmCacheData = localStorage.getItem(dmmCacheKey) ? JSON.parse(localStorage.getItem(dmmCacheKey)) : {}; - dmmCacheData[this.carNum] = { - type, - url - }; - localStorage.setItem(dmmCacheKey, JSON.stringify(dmmCacheData)); - return newItems; - } - clog.debug(`[${name2}] API 返回结果数 ${response.result.result_count},但无精确匹配的 Content ID。`); - } - clog.warn("所有关键词尝试均未找到匹配的Content ID, 解析Dmm视频失败"); - const $btn = $("#fanzaBtn"); - $btn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`); - $btn.attr("title", "未查询到, 点击前往搜索页"); - $btn.css("backgroundColor", "#de3333"); - return null; - } - - async _extractTrailerLinks({contentId, serviceCode, floorCode}) { - const trailerPageUrl = `https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=${contentId}/mtype=AhRVShI_/service=${serviceCode}/floor=${floorCode}/mode=/`, - htmlContent = await gmHttp.get(trailerPageUrl, null, { - "accept-language": "ja-JP,ja;q=0.9", - Cookie: "age_check_done=1" - }); - if ("string" != typeof htmlContent) { - clog.error(htmlContent); - throw new Error("解析播放页内容失败, 非文本内容"); - } - if (htmlContent.includes("このサービスはお住まいの地域からは")) throw new Error("节点不可用,请将DMM域名分流到日本ip"); - const match = htmlContent.match(/const\s+args\s+=\s+(.*);/); - if (!match) throw new Error("未在脚本中找到 const args = ... 变量"); - let bitrates; - try { - ({bitrates} = JSON.parse(match[1])); - } catch (e) { - throw new Error(`解析播放器脚本 JSON 失败: ${e.message}`); - } - const finalQualityMap = {}, qualityKeys = qualityOptions.map(((o) => o.quality)).join("|"), - qualityNameRegex = new RegExp(`(${qualityKeys})\\.mp4$`); - if (!Array.isArray(bitrates)) { - clog.error("解析画质链接失败: bitrates 字段不是一个数组或不存在"); - throw new Error("解析画质链接失败: bitrates 字段不是一个数组或不存在"); - } - clog.debug("原始数据返回:", bitrates); - for (const item of bitrates) { - const url = null == item ? void 0 : item.src; - if (!url || "string" != typeof url || !url.endsWith(".mp4")) continue; - const qualityMatch = url.match(qualityNameRegex); - let qualityKey = ""; - qualityMatch && qualityMatch[1] && (qualityKey = qualityMatch[1]); - qualityKey && !finalQualityMap[qualityKey] && (finalQualityMap[qualityKey] = url); - } - if (0 === Object.keys(finalQualityMap).length) throw new Error("未找到匹配要求的预览画质视频"); - return finalQualityMap; - } - - async _searchViaGraphQL() { - clog.debug("尝试通过 GraphQL API 搜索视频..."); - const videoMap = await getDmmGraphQLVideoUrls(this.carNum); - if (videoMap && Object.keys(videoMap).length > 0) { - this._updateCache(videoMap); - clog.debug("GraphQL 成功解析出预览视频:", videoMap); - const dmmCacheKey = "jhs_other_site_dmm"; - const dmmCacheData = localStorage.getItem(dmmCacheKey) ? JSON.parse(localStorage.getItem(dmmCacheKey)) : {}; - dmmCacheData[this.carNum] = { - type: "graphql", - url: `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}` - }; - localStorage.setItem(dmmCacheKey, JSON.stringify(dmmCacheData)); - return videoMap; - } - return null; - } - - async fetchVideo() { - const cachedResult = this._checkCache(); - if (cachedResult) return cachedResult; - let contentItems; - try { - const testCarNum = this.carNum.toLowerCase(); - if (testCarNum.startsWith("heyzo") || /^(n\d+|\d+(-\d+)*)$/.test(testCarNum) || /^n\d+$/.test(testCarNum)) throw new Error("无码番号类型, 取消dmm解析"); - if (this.carNum.includes("VR-")) throw new Error("VR类型, 取消dmm解析"); - contentItems = await this._searchContentIds(); - } catch (e) { - clog.error("DMM API 搜索失败:", e); - } - if (!contentItems || 0 === contentItems.length) { - clog.warn("Affiliate API 未找到视频,尝试 GraphQL API..."); - const graphQLResult = await this._searchViaGraphQL(); - if (graphQLResult) return graphQLResult; - const $btn = $("#fanzaBtn"); - $btn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`); - $btn.attr("title", "未查询到, 点击前往搜索页"); - $btn.css("backgroundColor", "#de3333"); - return null; - } - try { - const finalVideoMap = await Promise.any(contentItems.map(((item) => this._extractTrailerLinks(item)))); - this._updateCache(finalVideoMap); - return finalVideoMap; - } catch (error) { - const errors = error.errors || [error]; - clog.error(`Affiliate 解析失败: ${errors[0] && errors[0].message || errors[0]}`, errors); - clog.warn("尝试 GraphQL API 作为 fallback..."); - const graphQLResult = await this._searchViaGraphQL(); - if (graphQLResult) return graphQLResult; - if (errors.some(((err) => err.message.includes("节点不可用")))) this.showErrorMessages && show.error("节点不可用,请将DMM域名分流到日本ip"); - else { - const displayError = errors[0].message || errors[0]; - this.showErrorMessages && show.error(`解析失败: ${displayError}`); - } - const $btn = $("#fanzaBtn"); - $btn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`); - $btn.attr("title", "未查询到, 点击前往搜索页"); - $btn.css("backgroundColor", "#de3333"); - return null; - } - } + class DmmVideoFetcher { + constructor(carNum2, showErrorMessages = true) { + this.carNum = carNum2; + this.showErrorMessages = showErrorMessages; } - const getDmmVideo = async (carNum2, showErrorMessages = true) => new DmmVideoFetcher(carNum2, showErrorMessages).fetchVideo(); + _checkCache() { + const cachedData = localStorage.getItem(CACHE_KEY) ? JSON.parse(localStorage.getItem(CACHE_KEY)) : {}; + if (cachedData[this.carNum]) { + clog.debug("缓存中存在预览视频信息", cachedData[this.carNum]); + return cachedData[this.carNum]; + } + return null; + } + + _updateCache(videoMap) { + const cachedData = localStorage.getItem(CACHE_KEY) ? JSON.parse(localStorage.getItem(CACHE_KEY)) : {}; + cachedData[this.carNum] = videoMap; + clog.debug("成功解析出预览视频并已缓存:", videoMap); + localStorage.setItem(CACHE_KEY, JSON.stringify(cachedData)); + } + + async _searchContentIds() { + const carNum2 = this.carNum, carNumNoHyphen = carNum2.replace(/-/g, ""), keywordAttempts = [{ + keyword: carNum2.replace("-", "00"), + name: "00-替换关键词" + }, { + keyword: carNum2, + name: "原始番号关键词" + }, { + keyword: carNumNoHyphen, + name: "无连字符关键词" + }], carNumLower = carNum2.toLowerCase(); + for (const attempt of keywordAttempts) { + const {keyword, name: name2} = attempt, currentTempCarNumLower = keyword.toLowerCase(), + apiUrl2 = `https://api.dmm.com/affiliate/v3/ItemList?${new URLSearchParams({ + api_id: "UrwskPfkqQ0DuVry2gYL", + affiliate_id: "10278-996", + output: "json", + site: "FANZA", + sort: "match", + keyword + }).toString()}`; + let response; + try { + response = await gmHttp.get(apiUrl2); + } catch (e) { + clog.error(`API 请求失败,跳过 ${name2}:`, e); + continue; + } + if (!response || !response.result || !response.result.result_count) { + clog.debug(`使用 ${name2} (${keyword}) 进行 API 搜索 返回无结果,尝试下一个关键词。`); + continue; + } + const newItems = []; + for (const item of response.result.items) { + if (newItems.length >= 2) break; + const contentId = item.content_id || "", makerProduct = item.maker_product || ""; + if (contentId.includes(currentTempCarNumLower.replace("-", "")) || carNumLower === makerProduct.toLowerCase() || contentId.includes(carNumNoHyphen.toLowerCase())) { + newItems.push({ + serviceCode: item.service_code, + floorCode: item.floor_code, + contentId, + pageUrl: item.URL + }); + clog.debug(`[${name2}] cid|makerProduct 匹配成功:`, contentId, makerProduct); + } + } + if (newItems.length > 0) { + clog.debug(`--- 成功通过 ${name2} 找到 Content IDs ---`); + const $btn2 = $("#fanzaBtn"); + let url = `https://www.dmm.co.jp/search/=/searchstr=${keyword}`, type = "single"; + if (newItems.length > 1) { + $btn2.attr("href", url); + $btn2.append('多结果'); + $btn2.css("backgroundColor", "#7bc73b"); + type = "multiple"; + } else { + url = newItems[0].pageUrl; + $btn2.attr("href", url); + $btn2.css("backgroundColor", "#7bc73b"); + } + const dmmCacheKey = "jhs_other_site_dmm", + dmmCacheData = localStorage.getItem(dmmCacheKey) ? JSON.parse(localStorage.getItem(dmmCacheKey)) : {}; + dmmCacheData[this.carNum] = { + type, + url + }; + localStorage.setItem(dmmCacheKey, JSON.stringify(dmmCacheData)); + return newItems; + } + clog.debug(`[${name2}] API 返回结果数 ${response.result.result_count},但无精确匹配的 Content ID。`); + } + clog.warn("所有关键词尝试均未找到匹配的Content ID, 解析Dmm视频失败"); + const $btn = $("#fanzaBtn"); + $btn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`); + $btn.attr("title", "未查询到, 点击前往搜索页"); + $btn.css("backgroundColor", "#de3333"); + return null; + } + + async _extractTrailerLinks({contentId, serviceCode, floorCode}) { + const trailerPageUrl = `https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=${contentId}/mtype=AhRVShI_/service=${serviceCode}/floor=${floorCode}/mode=/`, + htmlContent = await gmHttp.get(trailerPageUrl, null, { + "accept-language": "ja-JP,ja;q=0.9", + Cookie: "age_check_done=1" + }); + if ("string" != typeof htmlContent) { + clog.error(htmlContent); + throw new Error("解析播放页内容失败, 非文本内容"); + } + if (htmlContent.includes("このサービスはお住まいの地域からは")) throw new Error("节点不可用,请将DMM域名分流到日本ip"); + const match = htmlContent.match(/const\s+args\s+=\s+(.*);/); + if (!match) throw new Error("未在脚本中找到 const args = ... 变量"); + let bitrates; + try { + ({bitrates} = JSON.parse(match[1])); + } catch (e) { + throw new Error(`解析播放器脚本 JSON 失败: ${e.message}`); + } + const finalQualityMap = {}, qualityKeys = qualityOptions.map(((o) => o.quality)).join("|"), + qualityNameRegex = new RegExp(`(${qualityKeys})\\.mp4$`); + if (!Array.isArray(bitrates)) { + clog.error("解析画质链接失败: bitrates 字段不是一个数组或不存在"); + throw new Error("解析画质链接失败: bitrates 字段不是一个数组或不存在"); + } + clog.debug("原始数据返回:", bitrates); + for (const item of bitrates) { + const url = null == item ? void 0 : item.src; + if (!url || "string" != typeof url || !url.endsWith(".mp4")) continue; + const qualityMatch = url.match(qualityNameRegex); + let qualityKey = ""; + qualityMatch && qualityMatch[1] && (qualityKey = qualityMatch[1]); + qualityKey && !finalQualityMap[qualityKey] && (finalQualityMap[qualityKey] = url); + } + if (0 === Object.keys(finalQualityMap).length) throw new Error("未找到匹配要求的预览画质视频"); + return finalQualityMap; + } + + async _searchViaGraphQL() { + clog.debug("尝试通过 GraphQL API 搜索视频..."); + const videoMap = await getDmmGraphQLVideoUrls(this.carNum); + if (videoMap && Object.keys(videoMap).length > 0) { + this._updateCache(videoMap); + clog.debug("GraphQL 成功解析出预览视频:", videoMap); + const dmmCacheKey = "jhs_other_site_dmm"; + const dmmCacheData = localStorage.getItem(dmmCacheKey) ? JSON.parse(localStorage.getItem(dmmCacheKey)) : {}; + dmmCacheData[this.carNum] = { + type: "graphql", + url: `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}` + }; + localStorage.setItem(dmmCacheKey, JSON.stringify(dmmCacheData)); + return videoMap; + } + return null; + } + + async fetchVideo() { + const cachedResult = this._checkCache(); + if (cachedResult) return cachedResult; + let contentItems; + try { + const testCarNum = this.carNum.toLowerCase(); + if (testCarNum.startsWith("heyzo") || /^(n\d+|\d+(-\d+)*)$/.test(testCarNum) || /^n\d+$/.test(testCarNum)) throw new Error("无码番号类型, 取消dmm解析"); + if (this.carNum.includes("VR-")) throw new Error("VR类型, 取消dmm解析"); + contentItems = await this._searchContentIds(); + } catch (e) { + clog.error("DMM API 搜索失败:", e); + } + if (!contentItems || 0 === contentItems.length) { + clog.warn("Affiliate API 未找到视频,尝试 GraphQL API..."); + const graphQLResult = await this._searchViaGraphQL(); + if (graphQLResult) return graphQLResult; + const $btn = $("#fanzaBtn"); + $btn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`); + $btn.attr("title", "未查询到, 点击前往搜索页"); + $btn.css("backgroundColor", "#de3333"); + return null; + } + try { + const finalVideoMap = await Promise.any(contentItems.map(((item) => this._extractTrailerLinks(item)))); + this._updateCache(finalVideoMap); + return finalVideoMap; + } catch (error) { + const errors = error.errors || [error]; + clog.error(`Affiliate 解析失败: ${errors[0] && errors[0].message || errors[0]}`, errors); + clog.warn("尝试 GraphQL API 作为 fallback..."); + const graphQLResult = await this._searchViaGraphQL(); + if (graphQLResult) return graphQLResult; + if (errors.some(((err) => err.message.includes("节点不可用")))) this.showErrorMessages && show.error("节点不可用,请将DMM域名分流到日本ip"); + else { + const displayError = errors[0].message || errors[0]; + this.showErrorMessages && show.error(`解析失败: ${displayError}`); + } + const $btn = $("#fanzaBtn"); + $btn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`); + $btn.attr("title", "未查询到, 点击前往搜索页"); + $btn.css("backgroundColor", "#de3333"); + return null; + } + } + } + + const getDmmVideo = async (carNum2, showErrorMessages = true) => new DmmVideoFetcher(carNum2, showErrorMessages).fetchVideo(); class AutoPagePlugin extends BasePlugin { constructor() { super(...arguments); - __publicField2(this, "preloadDistance", 500); - __publicField2(this, "currentPage", this.getInitialPageNumber()); - __publicField2(this, "pageItems", []); + __publicField2(this, "preloadDistance", 500); + __publicField2(this, "currentPage", this.getInitialPageNumber()); + __publicField2(this, "pageItems", []); } getName() { return "AutoPagePlugin"; @@ -3489,8 +3489,8 @@ ${value}\r class SettingPlugin extends BasePlugin { constructor() { super(...arguments); - __publicField2(this, "folderName", "JHS-数据备份"); - __publicField2(this, "cacheItems", [{ + __publicField2(this, "folderName", "JHS-数据备份"); + __publicField2(this, "cacheItems", [{ key: "jhs_dmm_video", text: "🎥 预览视频缓存", title: "预览视频缓存" @@ -4848,7 +4848,7 @@ ${value}\r $("#autoPage").on("change", (async (event) => { const autoPage = $("#autoPage").is(":checked") ? YES : NO; await storageManager.saveSettingItem("autoPage", autoPage); - autoPage === YES ? ($("#sort-toggle-btn").hide(), $("#one-time-sort-btn").show()) : ($("#sort-toggle-btn").show(), $("#one-time-sort-btn").hide()); + autoPage === YES ? ($("#sort-toggle-btn").hide(), $("#one-time-sort-btn").show()) : ($("#sort-toggle-btn").show(), $("#one-time-sort-btn").hide()); })); $("#translateTitle").on("change", (async (event) => { const translateTitle = $("#translateTitle").is(":checked") ? YES : NO; @@ -5038,23 +5038,23 @@ ${value}\r isJavBus$1 && this.getBean("BusImgPlugin").logImageHeightsByRow(); } bindClick() { - const self = this; + const self = this; $(".side-menu-item").on("click", (function() { $(".side-menu-item").removeClass("active"); $(this).addClass("active"); $(".content-panel").hide(); const panelId = $(this).data("panel"); $("#" + panelId).show(); - if ("cache-panel" === panelId || "mark-delete-panel" === panelId) { + if ("cache-panel" === panelId || "mark-delete-panel" === panelId) { $("#saveBtn").hide(); - "cache-panel" === panelId ? $("#clean-all").show() : $("#clean-all").hide(); + "cache-panel" === panelId ? $("#clean-all").show() : $("#clean-all").hide(); } else { $("#saveBtn").show(); $("#clean-all").hide(); } - if ("mark-delete-panel" === panelId) { - self.loadMarkDeleteList(); - } + if ("mark-delete-panel" === panelId) { + self.loadMarkDeleteList(); + } })); $("#importBtn").on("click", ((event) => this.importData(event))); $("#exportBtn").on("click", ((event) => this.exportData(event))); @@ -5100,71 +5100,71 @@ ${value}\r } $widthInput.on("input", updateBorder); $colorPicker.on("input", updateBorder); - $("#markDeleteSelectAll").on("change", (function () { - const checked = $(this).prop("checked"); - $(".mark-delete-item-cb").prop("checked", checked); + $("#markDeleteSelectAll").on("change", (function () { + const checked = $(this).prop("checked"); + $(".mark-delete-item-cb").prop("checked", checked); + })); + $("#batchDeleteBtn").on("click", (async () => { + const markedList = await storageManager.forage.getItem("markedDeleteList") || []; + if (markedList.length === 0) { + show.error("没有标记删除的作品"); + return; + } + const checkedItems = []; + $(".mark-delete-item-cb:checked").each((function () { + const index = parseInt($(this).data("index")); + checkedItems.push(markedList[index]); })); - $("#batchDeleteBtn").on("click", (async () => { - const markedList = await storageManager.forage.getItem("markedDeleteList") || []; - if (markedList.length === 0) { - show.error("没有标记删除的作品"); - return; - } - const checkedItems = []; - $(".mark-delete-item-cb:checked").each((function () { - const index = parseInt($(this).data("index")); - checkedItems.push(markedList[index]); + if (checkedItems.length === 0) { + show.error("请至少选择一个作品"); + return; + } + utils.q(null, `确认一键删除 ${checkedItems.length} 个作品?此操作不可撤销!`, (async () => { + let loadObj = loading(); + try { + const matchPlugin = this.getBean("WangPan115MatchPlugin"); + const result = await matchPlugin.batchDeleteMarked(checkedItems, ((current, total, item, status) => { + loadObj.close(); + loadObj = loading(); + $(`#markDeleteProgress`).text(`正在删除: ${current}/${total} - ${item.carNum} ${status}`); })); - if (checkedItems.length === 0) { - show.error("请至少选择一个作品"); - return; - } - utils.q(null, `确认一键删除 ${checkedItems.length} 个作品?此操作不可撤销!`, (async () => { - let loadObj = loading(); - try { - const matchPlugin = this.getBean("WangPan115MatchPlugin"); - const result = await matchPlugin.batchDeleteMarked(checkedItems, ((current, total, item, status) => { - loadObj.close(); - loadObj = loading(); - $(`#markDeleteProgress`).text(`正在删除: ${current}/${total} - ${item.carNum} ${status}`); - })); - loadObj.close(); - const remaining = markedList.filter((item) => !checkedItems.some((ci) => ci.carNum === item.carNum)); - await storageManager.forage.setItem("markedDeleteList", remaining); - show.ok(`删除完成: 成功 ${result.deletedCount} 个,失败 ${result.failedCount} 个`); - this.loadMarkDeleteList(); - } catch (error) { - loadObj.close(); - console.error("批量删除失败:", error); - show.error(`批量删除失败: ${error.message || "网络错误"}`); - } + loadObj.close(); + const remaining = markedList.filter((item) => !checkedItems.some((ci) => ci.carNum === item.carNum)); + await storageManager.forage.setItem("markedDeleteList", remaining); + show.ok(`删除完成: 成功 ${result.deletedCount} 个,失败 ${result.failedCount} 个`); + this.loadMarkDeleteList(); + } catch (error) { + loadObj.close(); + console.error("批量删除失败:", error); + show.error(`批量删除失败: ${error.message || "网络错误"}`); + } + })); + })); + $(document).on("click", ".mark-delete-remove-btn", (async (event) => { + event.preventDefault(); + const $btn = $(event.currentTarget); + const index = parseInt($btn.data("index")); + const markedList = await storageManager.forage.getItem("markedDeleteList") || []; + if (index >= 0 && index < markedList.length) { + const item = markedList[index]; + utils.q(null, `确认从标记列表中移除 "${item.title}"?`, (async () => { + markedList.splice(index, 1); + await storageManager.forage.setItem("markedDeleteList", markedList); + show.ok(`已移除: ${item.title}`); + const self2 = this; + $(`.markDeleteSvg`).each((function () { + const $markBtn = $(this); + const $box2 = $markBtn.closest(".item"); + const {carNum: carNum2} = self2.getBoxCarInfo($box2); + if (carNum2 === item.carNum) { + $markBtn.removeClass("marked"); + $markBtn.attr("title", "标记删除"); + } })); - })); - $(document).on("click", ".mark-delete-remove-btn", (async (event) => { - event.preventDefault(); - const $btn = $(event.currentTarget); - const index = parseInt($btn.data("index")); - const markedList = await storageManager.forage.getItem("markedDeleteList") || []; - if (index >= 0 && index < markedList.length) { - const item = markedList[index]; - utils.q(null, `确认从标记列表中移除 "${item.title}"?`, (async () => { - markedList.splice(index, 1); - await storageManager.forage.setItem("markedDeleteList", markedList); - show.ok(`已移除: ${item.title}`); - const self2 = this; - $(`.markDeleteSvg`).each((function () { - const $markBtn = $(this); - const $box2 = $markBtn.closest(".item"); - const {carNum: carNum2} = self2.getBoxCarInfo($box2); - if (carNum2 === item.carNum) { - $markBtn.removeClass("marked"); - $markBtn.attr("title", "标记删除"); - } - })); - this.loadMarkDeleteList(); - })); - } - })); + this.loadMarkDeleteList(); + })); + } + })); } async saveForm() { let settingObj = await storageManager.getSetting(); @@ -5240,21 +5240,21 @@ ${value}\r this.getBean("BlacklistPlugin").reloadTable(); } - async loadMarkDeleteList() { - const markedList = await storageManager.forage.getItem("markedDeleteList") || []; - const $list = $("#markDeleteList"); - const $count = $("#markDeleteCount"); - $count.text(`共 ${markedList.length} 个作品`); - if (markedList.length === 0) { - $list.html('
暂无标记删除的作品
'); - return; - } - let html = ""; - for (let i = 0; i < markedList.length; i++) { - const item = markedList[i]; - const truncatedTitle = item.title && item.title.length > 30 ? item.title.substring(0, 30) + "..." : item.title; - const detailUrl = item.url ? item.url.startsWith("http") ? item.url : window.location.origin + item.url : "#"; - html += `
+ async loadMarkDeleteList() { + const markedList = await storageManager.forage.getItem("markedDeleteList") || []; + const $list = $("#markDeleteList"); + const $count = $("#markDeleteCount"); + $count.text(`共 ${markedList.length} 个作品`); + if (markedList.length === 0) { + $list.html('
暂无标记删除的作品
'); + return; + } + let html = ""; + for (let i = 0; i < markedList.length; i++) { + const item = markedList[i]; + const truncatedTitle = item.title && item.title.length > 30 ? item.title.substring(0, 30) + "..." : item.title; + const detailUrl = item.url ? item.url.startsWith("http") ? item.url : window.location.origin + item.url : "#"; + html += `
${item.carNum}
@@ -5263,11 +5263,11 @@ ${value}\r
✕
`; - } - html += `
`; - $list.html(html); - $("#markDeleteSelectAll").prop("checked", true); } + html += `
`; + $list.html(html); + $("#markDeleteSelectAll").prop("checked", true); + } addLabelTag(containerId, keyword) { const $tagBox = $(`${containerId} .tag-box`); let $label, color = "#333"; @@ -5606,12 +5606,12 @@ ${value}\r class TaskPlugin extends BasePlugin { constructor() { super(...arguments); - __publicField2(this, "singleTaskKey", "checkNewActressActorFilterCar"); - __publicField2(this, "taskConfig", null); - __publicField2(this, "storageQueue", new AsyncQueue()); - __publicField2(this, "lastCheckFavoriteActressTimeKey", "jhs_time_checkFavoriteActress"); - __publicField2(this, "lastCheckBlacklistTimeKey", "jhs_time_checkBlacklist"); - __publicField2(this, "lastCheckNewVideoTimeKey", "jhs_time_checkNewVideo"); + __publicField2(this, "singleTaskKey", "checkNewActressActorFilterCar"); + __publicField2(this, "taskConfig", null); + __publicField2(this, "storageQueue", new AsyncQueue()); + __publicField2(this, "lastCheckFavoriteActressTimeKey", "jhs_time_checkFavoriteActress"); + __publicField2(this, "lastCheckBlacklistTimeKey", "jhs_time_checkBlacklist"); + __publicField2(this, "lastCheckNewVideoTimeKey", "jhs_time_checkNewVideo"); } getName() { return "TaskPlugin"; @@ -5951,8 +5951,8 @@ ${value}\r class NewVideoPlugin extends BasePlugin { constructor() { super(...arguments); - __publicField2(this, "currentPage", 1); - __publicField2(this, "pageSize", 30); + __publicField2(this, "currentPage", 1); + __publicField2(this, "pageSize", 30); } getName() { return "NewVideoPlugin"; @@ -6491,7 +6491,7 @@ ${value}\r class HistoryPlugin extends BasePlugin { constructor() { super(...arguments); - __publicField2(this, "tableObj", null); + __publicField2(this, "tableObj", null); } getName() { return "HistoryPlugin"; @@ -7105,42 +7105,42 @@ ${value}\r if (!$videoEl.length) return; const $videoContainer = $videoEl.parent(); $videoContainer.css("position", "relative"); - const videoEl = $videoEl[0]; - let carNum2 = this.getPageInfo().carNum; - const jhs_videoMuted = localStorage.getItem("jhs_videoMuted"); - if (jhs_videoMuted) videoEl.muted = "yes" === jhs_videoMuted; + const videoEl = $videoEl[0]; + let carNum2 = this.getPageInfo().carNum; + const jhs_videoMuted = localStorage.getItem("jhs_videoMuted"); + if (jhs_videoMuted) videoEl.muted = "yes" === jhs_videoMuted; videoEl.addEventListener("volumechange", (function() { localStorage.setItem("jhs_videoMuted", videoEl.muted ? "yes" : "no"); })); - const CACHE_KEY2 = "jhs_dmm_video"; - let dmmErrorRetryCount = 0; - const attachVideoErrorHandler = () => { - videoEl.addEventListener("error", async function onVideoError(e) { - if (dmmErrorRetryCount >= 2) return; - dmmErrorRetryCount++; - clog.warn(`预览视频加载失败(第${dmmErrorRetryCount}次),清除缓存后重试...`); - const cachedData = localStorage.getItem(CACHE_KEY2) ? JSON.parse(localStorage.getItem(CACHE_KEY2)) : {}; - delete cachedData[carNum2]; - localStorage.setItem(CACHE_KEY2, JSON.stringify(cachedData)); - const freshMap = await getDmmVideo(carNum2); - if (!freshMap) { - show.error("预览视频获取失败,请稍后重试"); - return; - } - const storedQuality = await storageManager.getSetting("videoQuality"); - const quality = selectDefaultQuality(Object.keys(freshMap), storedQuality); - const freshUrl = freshMap[quality]; - const savedTime = videoEl.currentTime; - $videoEl.attr("src", freshUrl); - videoEl.load(); - if (savedTime > 0 && Number.isFinite(savedTime)) { - videoEl.currentTime = savedTime; - } - videoEl.play(); - }, {once: false}); - }; - attachVideoErrorHandler(); - videoEl.play(); + const CACHE_KEY2 = "jhs_dmm_video"; + let dmmErrorRetryCount = 0; + const attachVideoErrorHandler = () => { + videoEl.addEventListener("error", async function onVideoError(e) { + if (dmmErrorRetryCount >= 2) return; + dmmErrorRetryCount++; + clog.warn(`预览视频加载失败(第${dmmErrorRetryCount}次),清除缓存后重试...`); + const cachedData = localStorage.getItem(CACHE_KEY2) ? JSON.parse(localStorage.getItem(CACHE_KEY2)) : {}; + delete cachedData[carNum2]; + localStorage.setItem(CACHE_KEY2, JSON.stringify(cachedData)); + const freshMap = await getDmmVideo(carNum2); + if (!freshMap) { + show.error("预览视频获取失败,请稍后重试"); + return; + } + const storedQuality = await storageManager.getSetting("videoQuality"); + const quality = selectDefaultQuality(Object.keys(freshMap), storedQuality); + const freshUrl = freshMap[quality]; + const savedTime = videoEl.currentTime; + $videoEl.attr("src", freshUrl); + videoEl.load(); + if (savedTime > 0 && Number.isFinite(savedTime)) { + videoEl.currentTime = savedTime; + } + videoEl.play(); + }, {once: false}); + }; + attachVideoErrorHandler(); + videoEl.play(); const dmmVideoMap = await getDmmVideo(carNum2); let $bottomToolbar = $("
").attr("id", "video-bottom-toolbar").css({ display: "flex", @@ -7198,9 +7198,9 @@ ${value}\r const currentTime = videoEl.currentTime; $videoEl.attr("src", videoSrc); videoEl.load(); - if (currentTime > 0 && Number.isFinite(currentTime)) { - videoEl.currentTime = currentTime; - } + if (currentTime > 0 && Number.isFinite(currentTime)) { + videoEl.currentTime = currentTime; + } await videoEl.play(); $bottomToolbar.find(".video-control-btn").removeClass("active").css({ "background-color": "#fff", @@ -7265,15 +7265,15 @@ ${value}\r return (this.isMac ? event.metaKey : event.ctrlKey) === ctrl && event.shiftKey === shift && event.altKey === alt && event.key.toLowerCase() === key; } }; - __publicField2(_HotkeyManager, "isMac", 0 === navigator.platform.indexOf("Mac")); - __publicField2(_HotkeyManager, "registerHotKeyMap", /* @__PURE__ */ new Map()); - __publicField2(_HotkeyManager, "handleKeydown", ((event) => { + __publicField2(_HotkeyManager, "isMac", 0 === navigator.platform.indexOf("Mac")); + __publicField2(_HotkeyManager, "registerHotKeyMap", /* @__PURE__ */ new Map()); + __publicField2(_HotkeyManager, "handleKeydown", ((event) => { for (const [id, data] of _HotkeyManager.registerHotKeyMap) { let hotkeyString = data.hotkeyString, callback = data.callback; _HotkeyManager.judgeHotkey(hotkeyString, event) && callback(event); } })); - __publicField2(_HotkeyManager, "handleKeyup", ((event) => { + __publicField2(_HotkeyManager, "handleKeyup", ((event) => { for (const [id, data] of _HotkeyManager.registerHotKeyMap) { let hotkeyString = data.hotkeyString, keyupCallback = data.keyupCallback; keyupCallback && (_HotkeyManager.judgeHotkey(hotkeyString, event) && keyupCallback(event)); @@ -7385,154 +7385,151 @@ ${value}\r } } - class JavPackSubtitle { - static escapeHtml(value = "") { - return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); - } + class JavPackSubtitle { + static escapeHtml(value = "") { + return String(value).replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """); + } - static sanitizeName(value = "") { - return String(value).replace(/[\\/:*?"<>|]/g, "").replace(/\s+/g, " ").trim(); - } + static sanitizeName(value = "") { + return String(value).replace(/[\\/:*?"<>|]/g, "").replace(/\s+/g, " ").trim(); + } - static isSubtitleFile(file = {}) { - return /\.(srt|ass|ssa|vtt|sub)$/i.test(file.n || ""); - } + static isSubtitleFile(file = {}) { + return /\.(srt|ass|ssa|vtt|sub)$/i.test(file.n || ""); + } - static buildDefaultKeyword(details = {}) { - return this.sanitizeName(details.code || "") || this.sanitizeName([details.code, details.title].filter(Boolean).join(" ")) || document.title; - } + static buildDefaultKeyword(details = {}) { + return this.sanitizeName(details.code || "") || this.sanitizeName([details.code, details.title].filter(Boolean).join(" ")) || document.title; + } - static buildSearchKeywords(details = {}) { - return [this.buildDefaultKeyword(details)].filter(Boolean); - } + static buildSearchKeywords(details = {}) { + return [this.buildDefaultKeyword(details)].filter(Boolean); + } - static buildSubtitleBaseName(details = {}) { - var _a; - if ((_a = window.JavPackMatch115Console) == null ? void 0 : _a.buildRename) { - return window.JavPackMatch115Console.buildRename(details, [{n: `${details.code || "subtitle"}.mp4`}]); - } - return this.sanitizeName([details.code, details.title].filter(Boolean).join(" ")) || this.buildDefaultKeyword(details); - } + static buildSubtitleBaseName(details = {}) { + var _a; + if ((_a = window.JavPackMatch115Console) == null ? void 0 : _a.buildRename) { + return window.JavPackMatch115Console.buildRename(details, [{n: `${details.code || "subtitle"}.mp4`}]); + } + return this.sanitizeName([details.code, details.title].filter(Boolean).join(" ")) || this.buildDefaultKeyword(details); + } - static buildSubtitleFilename(details = {}, item = {}) { - const ext = this.sanitizeName(item.ext || "srt").replace(/^\.+/, "") || "srt"; - return `${this.buildSubtitleBaseName(details)}.${ext}`; - } + static buildSubtitleFilename(details = {}, item = {}) { + const ext = this.sanitizeName(item.ext || "srt").replace(/^\.+/, "") || "srt"; + return `${this.buildSubtitleBaseName(details)}.${ext}`; + } - static clearPreviewCache() { - this.previewCache.clear(); - } + static clearPreviewCache() { + this.previewCache.clear(); + } - static async checkSubInCloud(req115, cid) { - if (!req115 || !cid) return false; - const res = await req115.filesAll(cid); - return ((res == null ? void 0 : res.data) || []).some((file) => this.isSubtitleFile(file)); - } + static async checkSubInCloud(req115, cid) { + if (!req115 || !cid) return false; + const res = await req115.filesAll(cid); + return ((res == null ? void 0 : res.data) || []).some((file) => this.isSubtitleFile(file)); + } - static getReq115() { - return window.Req115 || null; - } + static getReq115() { + return window.Req115 || null; + } - static getTargetCid() { - var _a, _b; - return ((_a = document.querySelector(".x-match-cont .zymatch-item [data-cid]")) == null ? void 0 : _a.dataset.cid) || ((_b = document.querySelector(".x-match-cont .zymatch-item")) == null ? void 0 : _b.dataset.cid) || ""; - } + static getTargetCid() { + var _a, _b; + return ((_a = document.querySelector(".x-match-cont .zymatch-item [data-cid]")) == null ? void 0 : _a.dataset.cid) || ((_b = document.querySelector(".x-match-cont .zymatch-item")) == null ? void 0 : _b.dataset.cid) || ""; + } - static async find115Cid(code) { - if (!code) return ""; - const req115 = this.getReq115(); - if (!req115) return ""; - try { - const {data} = await req115.filesSearchAllFolders(code); - if (!(data == null ? void 0 : data.length)) return ""; - const exact = data.find((f) => f.n === code); - if (exact) return exact.cid; - const upper = data.find((f) => f.n.toUpperCase() === code.toUpperCase()); - if (upper) return upper.cid; - const contains = data.find((f) => f.n.toUpperCase().includes(code.toUpperCase())); - return (contains == null ? void 0 : contains.cid) || ""; - } catch (e) { - console.warn("[JavPackSubtitle] 115搜索目录失败:", e.message); - return ""; - } - } + static async find115Cid(code) { + if (!code) return ""; + const req115 = this.getReq115(); + if (!req115) return ""; + try { + const {data} = await req115.filesSearchAllFolders(code); + if (!(data == null ? void 0 : data.length)) return ""; + const exact = data.find((f) => f.n === code); + if (exact) return exact.cid; + const upper = data.find((f) => f.n.toUpperCase() === code.toUpperCase()); + if (upper) return upper.cid; + const contains = data.find((f) => f.n.toUpperCase().includes(code.toUpperCase())); + return (contains == null ? void 0 : contains.cid) || ""; + } catch (e) { + console.warn("[JavPackSubtitle] 115搜索目录失败:", e.message); + return ""; + } + } - static async preload115Matches(code) { - this.cached115Matches = []; - this.current115Cids = []; - if (!code) return; - const req115 = this.getReq115(); - if (!req115) return; - try { - const {data} = await req115.filesSearchAllFolders(code); - if (!(data == null ? void 0 : data.length)) return; - const codeUpper = code.toUpperCase(); - const matches = data.filter((f) => f.n.toUpperCase().includes(codeUpper)).map((f) => ({ - cid: f.cid, - name: f.n - })).sort((a, b) => { - const aExact = a.name.toUpperCase() === codeUpper; - const bExact = b.name.toUpperCase() === codeUpper; - if (aExact && !bExact) return -1; - if (!aExact && bExact) return 1; - return a.name.localeCompare(b.name); - }); - this.cached115Matches = matches; - this.current115Cids = matches.length === 1 ? [matches[0].cid] : matches.map((m) => m.cid); - this.last115Code = code; - } catch (e) { - console.warn("[JavPackSubtitle] 115预加载目录失败:", e.message); - } - } + static async preload115Matches(code) { + this.cached115Matches = []; + this.current115Cids = []; + if (!code) return; + const req115 = this.getReq115(); + if (!req115) return; + try { + const {data} = await req115.filesSearchAllFolders(code); + if (!(data == null ? void 0 : data.length)) return; + const codeUpper = code.toUpperCase(); + const matches = data.filter((f) => f.n.toUpperCase().includes(codeUpper)).map((f) => ({ + cid: f.cid, + name: f.n + })).sort((a, b) => { + const aExact = a.name.toUpperCase() === codeUpper; + const bExact = b.name.toUpperCase() === codeUpper; + if (aExact && !bExact) return -1; + if (!aExact && bExact) return 1; + return a.name.localeCompare(b.name); + }); + this.cached115Matches = matches; + this.current115Cids = matches.length === 1 ? [matches[0].cid] : matches.map((m) => m.cid); + this.last115Code = code; + } catch (e) { + console.warn("[JavPackSubtitle] 115预加载目录失败:", e.message); + } + } - static ensureDetailButton({details, getTargetCid, getSubtitleEntries} = {}) { - const panel = document.querySelector(".movie-panel-info"); - const buttons = panel == null ? void 0 : panel.querySelector(".panel-block:last-child .buttons"); - if (!buttons || document.getElementById(this.BTN_ID)) return; - const btn = document.createElement("button"); - btn.id = this.BTN_ID; - btn.type = "button"; - btn.className = "button is-small is-info x-subtitle-search"; - btn.textContent = "字幕搜索"; - const refreshSubtitleFileTip = () => { - const groups = /* @__PURE__ */ new Map(); - ((getSubtitleEntries == null ? void 0 : getSubtitleEntries()) || []).forEach(({ - directory, - files = [] - }) => { - const path = String(directory || "").trim(); - if (!path) return; - if (!groups.has(path)) groups.set(path, /* @__PURE__ */ new Set()); - files.map((file) => String((file == null ? void 0 : file.n) || (file == null ? void 0 : file.name) || file || "").trim()).filter(Boolean).forEach((name2) => groups.get(path).add(name2)); - }); - btn.title = groups.size ? ["已有字幕文件:", ...[...groups].flatMap(([path, files]) => [ - `目录:${path}`, - ...[...files].map((name2) => ` - ${name2}`) - ])].join("\n") : "暂无已匹配的字幕文件"; - btn.textContent = groups.size ? "已有字幕" : "字幕搜索"; - btn.classList.toggle("is-success", Boolean(groups.size)); - btn.classList.toggle("is-info", !groups.size); - }; - btn.addEventListener("mouseenter", refreshSubtitleFileTip); - btn.addEventListener("focus", refreshSubtitleFileTip); - btn.addEventListener("click", (e) => { - e.preventDefault(); - e.stopPropagation(); - this.openSearchModal({details, getTargetCid}); - }); - buttons.appendChild(btn); - refreshSubtitleFileTip(); - const matchRoot = document.querySelector(".x-match-cont"); - if (matchRoot) new MutationObserver(refreshSubtitleFileTip).observe(matchRoot, { - childList: true, - subtree: true, - attributes: true, - attributeFilter: ["data-has-subtitle", "data-subtitle-files"] - }); - } + static ensureDetailButton({details, getTargetCid, getSubtitleEntries} = {}) { + const panel = document.querySelector(".movie-panel-info"); + const buttons = panel == null ? void 0 : panel.querySelector(".panel-block:last-child .buttons"); + if (!buttons || document.getElementById(this.BTN_ID)) return; + const btn = document.createElement("button"); + btn.id = this.BTN_ID; + btn.type = "button"; + btn.className = "button is-small is-info x-subtitle-search"; + btn.textContent = "字幕搜索"; + const refreshSubtitleFileTip = () => { + const groups = /* @__PURE__ */ new Map(); + ((getSubtitleEntries == null ? void 0 : getSubtitleEntries()) || []).forEach(({directory, files = []}) => { + const path = String(directory || "").trim(); + if (!path) return; + if (!groups.has(path)) groups.set(path, /* @__PURE__ */ new Set()); + files.map((file) => String((file == null ? void 0 : file.n) || (file == null ? void 0 : file.name) || file || "").trim()).filter(Boolean).forEach((name2) => groups.get(path).add(name2)); + }); + btn.title = groups.size ? ["已有字幕文件:", ...[...groups].flatMap(([path, files]) => [ + `目录:${path}`, + ...[...files].map((name2) => ` - ${name2}`) + ])].join("\n") : "暂无已匹配的字幕文件"; + btn.textContent = groups.size ? "已有字幕" : "字幕搜索"; + btn.classList.toggle("is-success", Boolean(groups.size)); + btn.classList.toggle("is-info", !groups.size); + }; + btn.addEventListener("mouseenter", refreshSubtitleFileTip); + btn.addEventListener("focus", refreshSubtitleFileTip); + btn.addEventListener("click", (e) => { + e.preventDefault(); + e.stopPropagation(); + this.openSearchModal({details, getTargetCid}); + }); + buttons.appendChild(btn); + refreshSubtitleFileTip(); + const matchRoot = document.querySelector(".x-match-cont"); + if (matchRoot) new MutationObserver(refreshSubtitleFileTip).observe(matchRoot, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["data-has-subtitle", "data-subtitle-files"] + }); + } - static modalTemplate(defaultKw) { - return ` + static modalTemplate(defaultKw) { + return `
@@ -7557,218 +7554,218 @@ ${value}\r
`; - } + } - static openSearchModal({details = {}, getTargetCid} = {}) { - var _a; - const defaultKw = this.buildDefaultKeyword(details); - this.currentDetails = details; - (_a = document.getElementById(this.MODAL_ID)) == null ? void 0 : _a.remove(); - const overlay = document.createElement("div"); - overlay.id = this.MODAL_ID; - overlay.className = "pdb-sub-overlay"; - overlay.innerHTML = this.modalTemplate(defaultKw); - document.body.appendChild(overlay); - const contentWrap = overlay.querySelector(".pdb-sub-content"); - const previewBox = overlay.querySelector(".pdb-sub-textarea"); - const statusNode = overlay.querySelector("#preview-status"); - const input = overlay.querySelector("#sub-search-input"); - const dirWrap = overlay.querySelector("#sub-115-wrap"); - const dirSelect = overlay.querySelector("#sub-115-select"); - const refresh115DirUI = () => { - if (!this.cached115Matches.length) { - dirWrap.style.display = "none"; - return; - } - dirWrap.style.display = ""; - dirSelect.innerHTML = this.cached115Matches.map((m) => { - const checked = this.current115Cids.includes(m.cid) ? " checked" : ""; - const displayName = m.name.length > 26 ? m.name.slice(0, 26) + "..." : m.name; - return ``; - }).join(""); - }; - dirSelect.addEventListener("change", () => { - this.current115Cids = Array.from(dirSelect.querySelectorAll("input:checked")).map((cb) => cb.value); - }); - if (this.last115Code === details.code && this.cached115Matches.length) { - refresh115DirUI(); - } else { - this.preload115Matches(details.code).then(refresh115DirUI); - } - const closeModal = () => { - this.clearPreviewCache(); - previewBox.value = ""; - overlay.remove(); - }; - overlay.querySelector("#sub-close-btn").addEventListener("click", closeModal); - overlay.addEventListener("click", (e) => { - if (e.target === overlay) closeModal(); - }); - const performSearch = async (kw) => { - if (!kw) return; - contentWrap.innerHTML = '
正在搜索字幕来源,请稍候...
'; - previewBox.value = ""; - statusNode.textContent = "暂无预览"; - try { - const results = await Promise.allSettled([ - this.searchXunlei(kw), - this.searchSubtitleCat(kw) - ]); - const dataList = results.flatMap((result) => result.status === "fulfilled" ? result.value : []); - const chineseResults = this.sortResults(dataList, kw, details); - if (!chineseResults.length) { - contentWrap.innerHTML = '
未找到简中或繁中字幕,请尝试使用完整番号搜索
'; - return; - } - this.renderTable({ - container: contentWrap, - dataList: chineseResults, - previewBox, - statusNode, - overlay, - details, - getTargetCid, - kw - }); - if (kw.toUpperCase() !== (details.code || "").toUpperCase()) { - this.preload115Matches(kw).then(refresh115DirUI); - } - } catch (e) { - console.error("字幕搜索失败:", e); - contentWrap.innerHTML = '
搜索失败,请稍后重试
'; - } - }; - overlay.querySelector("#sub-search-btn").addEventListener("click", () => performSearch(input.value.trim())); - input.addEventListener("keypress", (e) => { - if (e.key === "Enter") performSearch(input.value.trim()); - }); - performSearch(defaultKw); + static openSearchModal({details = {}, getTargetCid} = {}) { + var _a; + const defaultKw = this.buildDefaultKeyword(details); + this.currentDetails = details; + (_a = document.getElementById(this.MODAL_ID)) == null ? void 0 : _a.remove(); + const overlay = document.createElement("div"); + overlay.id = this.MODAL_ID; + overlay.className = "pdb-sub-overlay"; + overlay.innerHTML = this.modalTemplate(defaultKw); + document.body.appendChild(overlay); + const contentWrap = overlay.querySelector(".pdb-sub-content"); + const previewBox = overlay.querySelector(".pdb-sub-textarea"); + const statusNode = overlay.querySelector("#preview-status"); + const input = overlay.querySelector("#sub-search-input"); + const dirWrap = overlay.querySelector("#sub-115-wrap"); + const dirSelect = overlay.querySelector("#sub-115-select"); + const refresh115DirUI = () => { + if (!this.cached115Matches.length) { + dirWrap.style.display = "none"; + return; } - - static requestText(url) { - return window.gmHttp.get(url); + dirWrap.style.display = ""; + dirSelect.innerHTML = this.cached115Matches.map((m) => { + const checked = this.current115Cids.includes(m.cid) ? " checked" : ""; + const displayName = m.name.length > 26 ? m.name.slice(0, 26) + "..." : m.name; + return ``; + }).join(""); + }; + dirSelect.addEventListener("change", () => { + this.current115Cids = Array.from(dirSelect.querySelectorAll("input:checked")).map((cb) => cb.value); + }); + if (this.last115Code === details.code && this.cached115Matches.length) { + refresh115DirUI(); + } else { + this.preload115Matches(details.code).then(refresh115DirUI); + } + const closeModal = () => { + this.clearPreviewCache(); + previewBox.value = ""; + overlay.remove(); + }; + overlay.querySelector("#sub-close-btn").addEventListener("click", closeModal); + overlay.addEventListener("click", (e) => { + if (e.target === overlay) closeModal(); + }); + const performSearch = async (kw) => { + if (!kw) return; + contentWrap.innerHTML = '
正在搜索字幕来源,请稍候...
'; + previewBox.value = ""; + statusNode.textContent = "暂无预览"; + try { + const results = await Promise.allSettled([ + this.searchXunlei(kw), + this.searchSubtitleCat(kw) + ]); + const dataList = results.flatMap((result) => result.status === "fulfilled" ? result.value : []); + const chineseResults = this.sortResults(dataList, kw, details); + if (!chineseResults.length) { + contentWrap.innerHTML = '
未找到简中或繁中字幕,请尝试使用完整番号搜索
'; + return; + } + this.renderTable({ + container: contentWrap, + dataList: chineseResults, + previewBox, + statusNode, + overlay, + details, + getTargetCid, + kw + }); + if (kw.toUpperCase() !== (details.code || "").toUpperCase()) { + this.preload115Matches(kw).then(refresh115DirUI); + } + } catch (e) { + console.error("字幕搜索失败:", e); + contentWrap.innerHTML = '
搜索失败,请稍后重试
'; } + }; + overlay.querySelector("#sub-search-btn").addEventListener("click", () => performSearch(input.value.trim())); + input.addEventListener("keypress", (e) => { + if (e.key === "Enter") performSearch(input.value.trim()); + }); + performSearch(defaultKw); + } - static async searchXunlei(kw) { - const root = await this.requestText(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name=${encodeURIComponent(kw)}`); - if (!root || root.code !== 0 || !Array.isArray(root.data)) return []; - const results = root.data.slice(0, 20).map((item) => this.normalizeXunleiResult(item)); - const labeledResults = await Promise.all(results.map((item) => this.labelXunleiChineseSubtitle(item))); - return labeledResults.filter(Boolean); + static requestText(url) { + return window.gmHttp.get(url); + } + + static async searchXunlei(kw) { + const root = await this.requestText(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name=${encodeURIComponent(kw)}`); + if (!root || root.code !== 0 || !Array.isArray(root.data)) return []; + const results = root.data.slice(0, 20).map((item) => this.normalizeXunleiResult(item)); + const labeledResults = await Promise.all(results.map((item) => this.labelXunleiChineseSubtitle(item))); + return labeledResults.filter(Boolean); + } + + static normalizeXunleiResult(item = {}) { + const rawLanguages = item.languages ?? item.Languages ?? item.langs ?? item.Langs ?? item.language ?? item.Language ?? []; + return { + ...item, + name: item.name || item.Name || item.extra_name || item.Title || "", + languages: Array.isArray(rawLanguages) ? rawLanguages : rawLanguages ? [String(rawLanguages)] : [], + ext: item.ext || item.Ext || item.format || item.Format || "srt", + url: item.url || item.Url || "", + provider: "迅雷" + }; + } + + static async labelXunleiChineseSubtitle(item) { + if (this.isWantedChineseSubtitle(item)) return item; + if (!item.url) return null; + try { + const language = this.detectChineseSubtitleLanguage(this.decodeSubtitle(await this.fetchBinaryCached(item.url))); + return language ? {...item, languages: [language]} : null; + } catch (_) { + return null; + } + } + + static detectChineseSubtitleLanguage(text = "") { + const sample = String(text).slice(0, 12e3); + if (!/[\u3400-\u9fff]/.test(sample)) return ""; + const simplifiedCount = (sample.match(/[为与个么开关东风头后发这边过还进时样气应实话说问题现经体万台农国门书车云]/g) || []).length; + const traditionalCount = (sample.match(/[為與個麼開關東風頭後發這邊過還進時樣氣應實話說問題現經體萬臺農國門書車雲]/g) || []).length; + if (simplifiedCount === 0 && traditionalCount === 0) return ""; + return traditionalCount > simplifiedCount ? "繁中" : "简中"; + } + + static async searchSubtitleCat(kw) { + const searchUrl = `${this.SUBTITLECAT_ORIGIN}/index.php?search=${encodeURIComponent(kw)}`; + const searchHtml = await this.requestText(searchUrl); + const entries = this.parseSubtitleCatSearch(searchHtml).slice(0, 20); + const languageGroups = await Promise.all(entries.map(async (entry) => { + try { + return this.parseSubtitleCatLanguages(await this.requestText(entry.detailUrl), entry); + } catch (_) { + return []; } + })); + return languageGroups.flat(); + } - static normalizeXunleiResult(item = {}) { - const rawLanguages = item.languages ?? item.Languages ?? item.langs ?? item.Langs ?? item.language ?? item.Language ?? []; - return { - ...item, - name: item.name || item.Name || item.extra_name || item.Title || "", - languages: Array.isArray(rawLanguages) ? rawLanguages : rawLanguages ? [String(rawLanguages)] : [], - ext: item.ext || item.Ext || item.format || item.Format || "srt", - url: item.url || item.Url || "", - provider: "迅雷" - }; - } + static parseSubtitleCatSearch(html) { + const doc = new DOMParser().parseFromString(html, "text/html"); + return [...doc.querySelectorAll("table.sub-table tbody tr")].flatMap((row) => { + var _a; + const link = row.querySelector('a[href*="subs/"]'); + const name2 = (_a = link == null ? void 0 : link.textContent) == null ? void 0 : _a.trim(); + if (!link || !name2) return []; + return [{ + name: name2, + detailUrl: new URL(link.getAttribute("href"), this.SUBTITLECAT_ORIGIN).href + }]; + }); + } - static async labelXunleiChineseSubtitle(item) { - if (this.isWantedChineseSubtitle(item)) return item; - if (!item.url) return null; - try { - const language = this.detectChineseSubtitleLanguage(this.decodeSubtitle(await this.fetchBinaryCached(item.url))); - return language ? {...item, languages: [language]} : null; - } catch (_) { - return null; - } - } + static parseSubtitleCatLanguages(html, entry) { + const doc = new DOMParser().parseFromString(html, "text/html"); + return [...doc.querySelectorAll(".sub-single")].flatMap((node) => { + var _a; + const languageCode = ((_a = node.querySelector("img.flag")) == null ? void 0 : _a.getAttribute("alt")) || ""; + const language = this.SUBTITLECAT_CHINESE_LANGUAGES.get(languageCode); + const link = node.querySelector("a.green-link[href$='.srt']"); + if (!language || !link) return []; + return [{ + name: entry.name, + languageCode, + languages: [language], + ext: "srt", + provider: "SubtitleCat", + url: new URL(link.getAttribute("href"), this.SUBTITLECAT_ORIGIN).href + }]; + }); + } - static detectChineseSubtitleLanguage(text = "") { - const sample = String(text).slice(0, 12e3); - if (!/[\u3400-\u9fff]/.test(sample)) return ""; - const simplifiedCount = (sample.match(/[为与个么开关东风头后发这边过还进时样气应实话说问题现经体万台农国门书车云]/g) || []).length; - const traditionalCount = (sample.match(/[為與個麼開關東風頭後發這邊過還進時樣氣應實話說問題現經體萬臺農國門書車雲]/g) || []).length; - if (simplifiedCount === 0 && traditionalCount === 0) return ""; - return traditionalCount > simplifiedCount ? "繁中" : "简中"; - } + static isWantedChineseSubtitle(item = {}) { + if (item.provider === "SubtitleCat") return this.SUBTITLECAT_CHINESE_LANGUAGES.has(item.languageCode); + const language = Array.isArray(item.languages) ? item.languages.join(" ") : String(item.languages || ""); + const name2 = `${item.name || ""} ${item.extra_name || ""}`; + return /(zh[-_]?cn|zh[-_]?tw|chs|cht|chinese\s*(simplified|traditional)|简体|繁体|繁體|简中|繁中)/i.test(`${language} ${name2}`); + } - static async searchSubtitleCat(kw) { - const searchUrl = `${this.SUBTITLECAT_ORIGIN}/index.php?search=${encodeURIComponent(kw)}`; - const searchHtml = await this.requestText(searchUrl); - const entries = this.parseSubtitleCatSearch(searchHtml).slice(0, 20); - const languageGroups = await Promise.all(entries.map(async (entry) => { - try { - return this.parseSubtitleCatLanguages(await this.requestText(entry.detailUrl), entry); - } catch (_) { - return []; - } - })); - return languageGroups.flat(); - } + static sortResults(dataList, kw = "", details = {}) { + const kwClean = kw.toLowerCase().replace(/[-_.\s]/g, ""); + const tokens = kw.toLowerCase().split(/[-_.\s]+/).filter((word) => word.length > 1); + const codeClean = String(details.code || kw).toLowerCase().replace(/[-_.\s]/g, ""); + return [...dataList].filter((item) => this.isWantedChineseSubtitle(item)).sort((a, b) => this.scoreResult(b, kwClean, tokens, codeClean) - this.scoreResult(a, kwClean, tokens, codeClean)); + } - static parseSubtitleCatSearch(html) { - const doc = new DOMParser().parseFromString(html, "text/html"); - return [...doc.querySelectorAll("table.sub-table tbody tr")].flatMap((row) => { - var _a; - const link = row.querySelector('a[href*="subs/"]'); - const name2 = (_a = link == null ? void 0 : link.textContent) == null ? void 0 : _a.trim(); - if (!link || !name2) return []; - return [{ - name: name2, - detailUrl: new URL(link.getAttribute("href"), this.SUBTITLECAT_ORIGIN).href - }]; - }); - } + static scoreResult(item, kwClean, tokens, codeClean = "") { + const name2 = (item.name || item.extra_name || "").toLowerCase(); + const compactName = name2.replace(/[-_.\s]/g, ""); + const lang = (item.languages && item.languages[0] || "").toLowerCase(); + let score = 0; + if (codeClean && compactName.includes(codeClean)) score += 800; + tokens.forEach((token) => { + if (name2.includes(token)) score += 50; + }); + if (kwClean && compactName.includes(kwClean)) score += 500; + if (/(zh|cn|chs|cht|chinese|简中|繁中)/i.test(lang) || /(zh|cn|chs|cht|chinese|简中|繁中)/i.test(name2)) score += 100; + if (item.ext === "srt" || item.ext === "ass") score += 20; + return score; + } - static parseSubtitleCatLanguages(html, entry) { - const doc = new DOMParser().parseFromString(html, "text/html"); - return [...doc.querySelectorAll(".sub-single")].flatMap((node) => { - var _a; - const languageCode = ((_a = node.querySelector("img.flag")) == null ? void 0 : _a.getAttribute("alt")) || ""; - const language = this.SUBTITLECAT_CHINESE_LANGUAGES.get(languageCode); - const link = node.querySelector("a.green-link[href$='.srt']"); - if (!language || !link) return []; - return [{ - name: entry.name, - languageCode, - languages: [language], - ext: "srt", - provider: "SubtitleCat", - url: new URL(link.getAttribute("href"), this.SUBTITLECAT_ORIGIN).href - }]; - }); - } - - static isWantedChineseSubtitle(item = {}) { - if (item.provider === "SubtitleCat") return this.SUBTITLECAT_CHINESE_LANGUAGES.has(item.languageCode); - const language = Array.isArray(item.languages) ? item.languages.join(" ") : String(item.languages || ""); - const name2 = `${item.name || ""} ${item.extra_name || ""}`; - return /(zh[-_]?cn|zh[-_]?tw|chs|cht|chinese\s*(simplified|traditional)|简体|繁体|繁體|简中|繁中)/i.test(`${language} ${name2}`); - } - - static sortResults(dataList, kw = "", details = {}) { - const kwClean = kw.toLowerCase().replace(/[-_.\s]/g, ""); - const tokens = kw.toLowerCase().split(/[-_.\s]+/).filter((word) => word.length > 1); - const codeClean = String(details.code || kw).toLowerCase().replace(/[-_.\s]/g, ""); - return [...dataList].filter((item) => this.isWantedChineseSubtitle(item)).sort((a, b) => this.scoreResult(b, kwClean, tokens, codeClean) - this.scoreResult(a, kwClean, tokens, codeClean)); - } - - static scoreResult(item, kwClean, tokens, codeClean = "") { - const name2 = (item.name || item.extra_name || "").toLowerCase(); - const compactName = name2.replace(/[-_.\s]/g, ""); - const lang = (item.languages && item.languages[0] || "").toLowerCase(); - let score = 0; - if (codeClean && compactName.includes(codeClean)) score += 800; - tokens.forEach((token) => { - if (name2.includes(token)) score += 50; - }); - if (kwClean && compactName.includes(kwClean)) score += 500; - if (/(zh|cn|chs|cht|chinese|简中|繁中)/i.test(lang) || /(zh|cn|chs|cht|chinese|简中|繁中)/i.test(name2)) score += 100; - if (item.ext === "srt" || item.ext === "ass") score += 20; - return score; - } - - static renderTable({container, dataList, previewBox, statusNode, overlay, details, getTargetCid, kw = ""}) { - const words = kw.split(/[-_.\s]+/).filter((word) => word.length > 1); - const highlightRegex = words.length ? new RegExp(`(${words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "gi") : null; - container.innerHTML = ` + static renderTable({container, dataList, previewBox, statusNode, overlay, details, getTargetCid, kw = ""}) { + const words = kw.split(/[-_.\s]+/).filter((word) => word.length > 1); + const highlightRegex = words.length ? new RegExp(`(${words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "gi") : null; + container.innerHTML = ` @@ -7784,27 +7781,27 @@ ${value}\r
`; - container.querySelectorAll(".sub-action-btn").forEach((btn) => { - btn.addEventListener("click", () => this.handleAction({ - btn, - item: dataList[btn.dataset.idx], - previewBox, - statusNode, - overlay, - details, - getTargetCid - })); - }); - } + container.querySelectorAll(".sub-action-btn").forEach((btn) => { + btn.addEventListener("click", () => this.handleAction({ + btn, + item: dataList[btn.dataset.idx], + previewBox, + statusNode, + overlay, + details, + getTargetCid + })); + }); + } - static renderRow(item, index, highlightRegex) { - var _a; - const lang = ((_a = item.languages) == null ? void 0 : _a[0]) || "未知"; - const name2 = item.name || item.extra_name || "未知字幕"; - const provider = item.provider || "迅雷"; - const providerClass = provider === "SubtitleCat" ? "pdb-sub-provider--subtitlecat" : "pdb-sub-provider--xunlei"; - const displayName = highlightRegex ? this.escapeHtml(name2).replace(highlightRegex, '$1') : this.escapeHtml(name2); - return ` + static renderRow(item, index, highlightRegex) { + var _a; + const lang = ((_a = item.languages) == null ? void 0 : _a[0]) || "未知"; + const name2 = item.name || item.extra_name || "未知字幕"; + const provider = item.provider || "迅雷"; + const providerClass = provider === "SubtitleCat" ? "pdb-sub-provider--subtitlecat" : "pdb-sub-provider--xunlei"; + const displayName = highlightRegex ? this.escapeHtml(name2).replace(highlightRegex, '$1') : this.escapeHtml(name2); + return ` ${this.escapeHtml(provider)} ${displayName} @@ -7817,614 +7814,614 @@ ${value}\r `; - } - - static async handleAction({btn, item, previewBox, statusNode, overlay, details, getTargetCid}) { - const action = btn.dataset.action; - const oldText = btn.textContent; - const url = item.url; - if (!url) return alert("无效的字幕下载直链"); - const filename = this.buildSubtitleFilename(details, item); - btn.textContent = "获取中..."; - btn.style.opacity = "0.6"; - try { - const buffer = await this.fetchBinaryCached(url); - const text = this.decodeSubtitle(buffer); - if (text.includes("NoSuchKey")) throw new Error("该字幕在迅雷云端已失效丢失 (NoSuchKey)"); - if (action === "preview") { - previewBox.value = text; - statusNode.textContent = filename; - this.setActivePreviewRow(overlay, btn); - } else if (action === "download") { - this.downloadBuffer(buffer, filename); - } else if (action === "upload") { - const jdbCid = (getTargetCid == null ? void 0 : getTargetCid()) || this.getTargetCid(); - let cids = jdbCid ? [jdbCid] : [...this.current115Cids]; - const req115 = this.getReq115(); - if (!cids.length || !req115) throw new Error("未在 115 找到匹配目录,请确认作品已下载到 115"); - btn.textContent = "直传中..."; - for (const cid of cids) { - await this.uploadSubtitle({req115, cid, filename, buffer}); - } - show.ok(`字幕已上传到 115(${cids.length}个目录)`); - const detailBtn = document.getElementById(this.BTN_ID); - if (detailBtn) { - detailBtn.classList.add("is-success"); - detailBtn.textContent = "已有字幕"; - } - window.dispatchEvent(new CustomEvent("JavDB_SubtitleUploaded", { - detail: { - code: details.code || "", - cid: String(cids[0]), - subtitle: {n: filename, s: buffer.byteLength} - } - })); - overlay.remove(); - this.clearPreviewCache(); - } - } catch (err) { - alert(`执行中止: ${err.message}`); - } finally { - btn.textContent = oldText; - btn.style.opacity = "1"; - } - } - - static setActivePreviewRow(overlay, btn) { - overlay.querySelectorAll(".pdb-sub-tr.is-previewing").forEach((row2) => { - row2.classList.remove("is-previewing"); - row2.removeAttribute("aria-current"); - }); - const row = btn.closest(".pdb-sub-tr"); - if (!row) return; - row.classList.add("is-previewing"); - row.setAttribute("aria-current", "true"); - } - - static decodeSubtitle(buffer) { - let text = new TextDecoder("utf-8").decode(buffer); - if ((text.match(/\uFFFD/g) || []).length > 3) text = new TextDecoder("gbk").decode(buffer); - return text; - } - - static fetchBinaryCached(url) { - if (this.previewCache.has(url)) return Promise.resolve(this.previewCache.get(url)); - return this.fetchBinary(url).then((buffer) => { - this.previewCache.set(url, buffer); - return buffer; - }); - } - - static fetchBinary(url) { - return new Promise((resolve, reject) => { - _GM_xmlhttpRequest({ - method: "GET", - url, - responseType: "arraybuffer", - onload: (res) => { - if (res.status === 200 && res.response) resolve(res.response); - else reject(new Error(`字幕流获取失败,HTTP_CODE: ${res.status}`)); - }, - onerror: () => reject(new Error("跨域网络请求被阻断")) - }); - }); - } - - static downloadBuffer(buffer, filename) { - const blob = new Blob([buffer], {type: "application/octet-stream"}); - const link = document.createElement("a"); - link.href = URL.createObjectURL(blob); - link.download = filename; - link.click(); - URL.revokeObjectURL(link.href); - } - - static uploadSubtitle(args) { - return args.req115.queueMutation("上传字幕", () => this.uploadSubtitleNow(args)); - } - - static async uploadSubtitleNow({req115, cid, filename, buffer}) { - const blob = new Blob([buffer], {type: "application/octet-stream"}); - const file = new File([blob], filename, {type: "application/octet-stream"}); - const initRes = await req115.sampleInitUpload({filename, filesize: file.size, cid}); - if (!initRes || !initRes.host && initRes.status !== 2 && initRes.statuscode !== 0) { - throw new Error((initRes == null ? void 0 : initRes.error_msg) || "获取115上传安全凭证被拦截"); - } - if (!initRes.host) return initRes; - let uploadRes = null; - for (let retry = 0; retry < 3; retry++) { - uploadRes = await req115.upload({...initRes, filename, file}); - if (uploadRes && uploadRes.state !== false) return uploadRes; - await new Promise((resolve) => setTimeout(resolve, 1500)); - } - throw new Error((uploadRes == null ? void 0 : uploadRes.error_msg) || (uploadRes == null ? void 0 : uploadRes.error) || "115 服务器拒绝接收回调"); - } } - __publicField(JavPackSubtitle, "BTN_ID", "x-subtitle-search-btn"); - __publicField(JavPackSubtitle, "MODAL_ID", "x-subtitle-modal"); - __publicField(JavPackSubtitle, "previewCache", /* @__PURE__ */ new Map()); - __publicField(JavPackSubtitle, "cached115Matches", []); - __publicField(JavPackSubtitle, "current115Cids", []); - __publicField(JavPackSubtitle, "last115Code", ""); - __publicField(JavPackSubtitle, "SUBTITLECAT_ORIGIN", "https://www.subtitlecat.com"); - __publicField(JavPackSubtitle, "SUBTITLECAT_CHINESE_LANGUAGES", /* @__PURE__ */ new Map([ - ["zh-CN", "简中"], - ["zh-TW", "繁中"] - ])); - window.JavPackSubtitle = JavPackSubtitle; - - class Req { - static _serializeToFormData(formData, data, parentKey = "") { - Object.entries(data).forEach(([key, val]) => { - const fullKey = parentKey ? `${parentKey}[${key}]` : key; - if (val === void 0) return; - if (val !== null && typeof val === "object" && !(val instanceof Blob || val instanceof Date)) { - this._serializeToFormData(formData, val, fullKey); - } else { - formData.append(fullKey, val instanceof Date ? val.toISOString() : val); - } - }); - } - - static request(config) { - let options; - if (typeof config === "string") { - options = {url: config}; - } else if (config !== null && typeof config === "object") { - options = {...config}; - } else { - return Promise.reject(new TypeError("Request config must be a string or an object")); - } - if (!options.url) return Promise.reject(new Error("Request URL is required")); - options.method = (options.method || this.DEFAULT_METHOD).toUpperCase(); - options.timeout || (options.timeout = this.DEFAULT_TIMEOUT); - const {params, method, data, signal} = options; - try { - const urlInstance = new URL(options.url, location.origin); - if (params) { - const cleanParams = this._isPlainObject(params) ? Object.fromEntries(Object.entries(params).filter(([_, v]) => v != null)) : params; - new URLSearchParams(cleanParams).forEach((v, k) => urlInstance.searchParams.set(k, v)); - } - options.url = urlInstance.toString(); - } catch { - return Promise.reject(new Error(`Invalid Request URL: ${options.url}`)); - } - if (method === "POST") { - options.responseType ?? (options.responseType = this.DEFAULT_POST_RESPONSE_TYPE); - if (this._isPlainObject(data) && !this._isRawBody(data)) { - const formData = new FormData(); - this._serializeToFormData(formData, data); - options.data = formData; - } - } else if (method === "GET") { - options.responseType ?? (options.responseType = options.url.split(/[?#]/)[0].endsWith(".json") ? "json" : this.DEFAULT_GET_RESPONSE_TYPE); - } - return new Promise((resolve, reject) => { - if (signal == null ? void 0 : signal.aborted) return reject(new DOMException("Aborted", "AbortError")); - const {url: targetUrl} = options; - let xhrHandler; - const handleAbort = () => xhrHandler == null ? void 0 : xhrHandler.abort(); - const finalize = (settleFn, ...args) => { - signal == null ? void 0 : signal.removeEventListener("abort", handleAbort); - settleFn(...args); - }; - xhrHandler = _GM_xmlhttpRequest({ - onabort: () => reject(new DOMException("Aborted", "AbortError")), - ontimeout: () => finalize(reject, new Error(`Timeout: ${targetUrl}`)), - onerror: () => finalize(reject, new Error(`Network Error: ${targetUrl}`)), - onload: ({ - status, - finalUrl, - response - }) => status >= 400 ? finalize(reject, new Error(`HTTP ${status}: ${targetUrl}`)) : finalize(resolve, method === "HEAD" ? finalUrl : response), - ...options, - params: void 0 - }); - signal == null ? void 0 : signal.addEventListener("abort", handleAbort, {once: true}); - }); - } - - static async tasks(initial, steps, signal) { - let ctx = initial; - for (const step of steps) { - if (signal == null ? void 0 : signal.aborted) throw new DOMException("Aborted", "AbortError"); - if (typeof ctx === "string" || (ctx == null ? void 0 : ctx.url)) ctx = await this.request(typeof ctx === "string" ? { - url: ctx, - signal - } : {...ctx, signal}); - ctx = await step(ctx); - } - return ctx; + static async handleAction({btn, item, previewBox, statusNode, overlay, details, getTargetCid}) { + const action = btn.dataset.action; + const oldText = btn.textContent; + const url = item.url; + if (!url) return alert("无效的字幕下载直链"); + const filename = this.buildSubtitleFilename(details, item); + btn.textContent = "获取中..."; + btn.style.opacity = "0.6"; + try { + const buffer = await this.fetchBinaryCached(url); + const text = this.decodeSubtitle(buffer); + if (text.includes("NoSuchKey")) throw new Error("该字幕在迅雷云端已失效丢失 (NoSuchKey)"); + if (action === "preview") { + previewBox.value = text; + statusNode.textContent = filename; + this.setActivePreviewRow(overlay, btn); + } else if (action === "download") { + this.downloadBuffer(buffer, filename); + } else if (action === "upload") { + const jdbCid = (getTargetCid == null ? void 0 : getTargetCid()) || this.getTargetCid(); + let cids = jdbCid ? [jdbCid] : [...this.current115Cids]; + const req115 = this.getReq115(); + if (!cids.length || !req115) throw new Error("未在 115 找到匹配目录,请确认作品已下载到 115"); + btn.textContent = "直传中..."; + for (const cid of cids) { + await this.uploadSubtitle({req115, cid, filename, buffer}); + } + show.ok(`字幕已上传到 115(${cids.length}个目录)`); + const detailBtn = document.getElementById(this.BTN_ID); + if (detailBtn) { + detailBtn.classList.add("is-success"); + detailBtn.textContent = "已有字幕"; + } + window.dispatchEvent(new CustomEvent("JavDB_SubtitleUploaded", { + detail: { + code: details.code || "", + cid: String(cids[0]), + subtitle: {n: filename, s: buffer.byteLength} + } + })); + overlay.remove(); + this.clearPreviewCache(); } + } catch (err) { + alert(`执行中止: ${err.message}`); + } finally { + btn.textContent = oldText; + btn.style.opacity = "1"; + } } - __publicField(Req, "DEFAULT_GET_RESPONSE_TYPE", "document"); - __publicField(Req, "DEFAULT_POST_RESPONSE_TYPE", "json"); - __publicField(Req, "DEFAULT_TIMEOUT", 3e4); - __publicField(Req, "DEFAULT_METHOD", "GET"); - __publicField(Req, "_isRawBody", (v) => v instanceof FormData || v instanceof Blob || v instanceof ArrayBuffer || v instanceof URLSearchParams); - __publicField(Req, "_isPlainObject", (v) => Object.prototype.toString.call(v) === "[object Object]"); + static setActivePreviewRow(overlay, btn) { + overlay.querySelectorAll(".pdb-sub-tr.is-previewing").forEach((row2) => { + row2.classList.remove("is-previewing"); + row2.removeAttribute("aria-current"); + }); + const row = btn.closest(".pdb-sub-tr"); + if (!row) return; + row.classList.add("is-previewing"); + row.setAttribute("aria-current", "true"); + } - class Drive115 extends Req { - static files(cid = "0", params = {}) { - return this.request({ - url: "https://webapi.115.com/files", - params: {cid, ...params} - }); + static decodeSubtitle(buffer) { + let text = new TextDecoder("utf-8").decode(buffer); + if ((text.match(/\uFFFD/g) || []).length > 3) text = new TextDecoder("gbk").decode(buffer); + return text; + } + + static fetchBinaryCached(url) { + if (this.previewCache.has(url)) return Promise.resolve(this.previewCache.get(url)); + return this.fetchBinary(url).then((buffer) => { + this.previewCache.set(url, buffer); + return buffer; + }); + } + + static fetchBinary(url) { + return new Promise((resolve, reject) => { + _GM_xmlhttpRequest({ + method: "GET", + url, + responseType: "arraybuffer", + onload: (res) => { + if (res.status === 200 && res.response) resolve(res.response); + else reject(new Error(`字幕流获取失败,HTTP_CODE: ${res.status}`)); + }, + onerror: () => reject(new Error("跨域网络请求被阻断")) + }); + }); + } + + static downloadBuffer(buffer, filename) { + const blob = new Blob([buffer], {type: "application/octet-stream"}); + const link = document.createElement("a"); + link.href = URL.createObjectURL(blob); + link.download = filename; + link.click(); + URL.revokeObjectURL(link.href); + } + + static uploadSubtitle(args) { + return args.req115.queueMutation("上传字幕", () => this.uploadSubtitleNow(args)); + } + + static async uploadSubtitleNow({req115, cid, filename, buffer}) { + const blob = new Blob([buffer], {type: "application/octet-stream"}); + const file = new File([blob], filename, {type: "application/octet-stream"}); + const initRes = await req115.sampleInitUpload({filename, filesize: file.size, cid}); + if (!initRes || !initRes.host && initRes.status !== 2 && initRes.statuscode !== 0) { + throw new Error((initRes == null ? void 0 : initRes.error_msg) || "获取115上传安全凭证被拦截"); + } + if (!initRes.host) return initRes; + let uploadRes = null; + for (let retry = 0; retry < 3; retry++) { + uploadRes = await req115.upload({...initRes, filename, file}); + if (uploadRes && uploadRes.state !== false) return uploadRes; + await new Promise((resolve) => setTimeout(resolve, 1500)); + } + throw new Error((uploadRes == null ? void 0 : uploadRes.error_msg) || (uploadRes == null ? void 0 : uploadRes.error) || "115 服务器拒绝接收回调"); + } + } + + __publicField(JavPackSubtitle, "BTN_ID", "x-subtitle-search-btn"); + __publicField(JavPackSubtitle, "MODAL_ID", "x-subtitle-modal"); + __publicField(JavPackSubtitle, "previewCache", /* @__PURE__ */ new Map()); + __publicField(JavPackSubtitle, "cached115Matches", []); + __publicField(JavPackSubtitle, "current115Cids", []); + __publicField(JavPackSubtitle, "last115Code", ""); + __publicField(JavPackSubtitle, "SUBTITLECAT_ORIGIN", "https://www.subtitlecat.com"); + __publicField(JavPackSubtitle, "SUBTITLECAT_CHINESE_LANGUAGES", /* @__PURE__ */ new Map([ + ["zh-CN", "简中"], + ["zh-TW", "繁中"] + ])); + window.JavPackSubtitle = JavPackSubtitle; + + class Req { + static _serializeToFormData(formData, data, parentKey = "") { + Object.entries(data).forEach(([key, val]) => { + const fullKey = parentKey ? `${parentKey}[${key}]` : key; + if (val === void 0) return; + if (val !== null && typeof val === "object" && !(val instanceof Blob || val instanceof Date)) { + this._serializeToFormData(formData, val, fullKey); + } else { + formData.append(fullKey, val instanceof Date ? val.toISOString() : val); } + }); + } - static filesSearch(search_value, params = {}) { - return this.request({ - url: "https://webapi.115.com/files/search", - params: {search_value, ...params} - }); + static request(config) { + let options; + if (typeof config === "string") { + options = {url: config}; + } else if (config !== null && typeof config === "object") { + options = {...config}; + } else { + return Promise.reject(new TypeError("Request config must be a string or an object")); + } + if (!options.url) return Promise.reject(new Error("Request URL is required")); + options.method = (options.method || this.DEFAULT_METHOD).toUpperCase(); + options.timeout || (options.timeout = this.DEFAULT_TIMEOUT); + const {params, method, data, signal} = options; + try { + const urlInstance = new URL(options.url, location.origin); + if (params) { + const cleanParams = this._isPlainObject(params) ? Object.fromEntries(Object.entries(params).filter(([_, v]) => v != null)) : params; + new URLSearchParams(cleanParams).forEach((v, k) => urlInstance.searchParams.set(k, v)); } - - static lixianTaskLists(page = 1) { - return this.request({ - url: "https://115.com/web/lixian/", - params: {ct: "lixian", ac: "task_lists", page} - }); + options.url = urlInstance.toString(); + } catch { + return Promise.reject(new Error(`Invalid Request URL: ${options.url}`)); + } + if (method === "POST") { + options.responseType ?? (options.responseType = this.DEFAULT_POST_RESPONSE_TYPE); + if (this._isPlainObject(data) && !this._isRawBody(data)) { + const formData = new FormData(); + this._serializeToFormData(formData, data); + options.data = formData; } + } else if (method === "GET") { + options.responseType ?? (options.responseType = options.url.split(/[?#]/)[0].endsWith(".json") ? "json" : this.DEFAULT_GET_RESPONSE_TYPE); + } + return new Promise((resolve, reject) => { + if (signal == null ? void 0 : signal.aborted) return reject(new DOMException("Aborted", "AbortError")); + const {url: targetUrl} = options; + let xhrHandler; + const handleAbort = () => xhrHandler == null ? void 0 : xhrHandler.abort(); + const finalize = (settleFn, ...args) => { + signal == null ? void 0 : signal.removeEventListener("abort", handleAbort); + settleFn(...args); + }; + xhrHandler = _GM_xmlhttpRequest({ + onabort: () => reject(new DOMException("Aborted", "AbortError")), + ontimeout: () => finalize(reject, new Error(`Timeout: ${targetUrl}`)), + onerror: () => finalize(reject, new Error(`Network Error: ${targetUrl}`)), + onload: ({ + status, + finalUrl, + response + }) => status >= 400 ? finalize(reject, new Error(`HTTP ${status}: ${targetUrl}`)) : finalize(resolve, method === "HEAD" ? finalUrl : response), + ...options, + params: void 0 + }); + signal == null ? void 0 : signal.addEventListener("abort", handleAbort, {once: true}); + }); + } - static labelList() { - return this.request({ - url: "https://webapi.115.com/label/list", - params: {keyword: "", limit: 11150} - }); - } + static async tasks(initial, steps, signal) { + let ctx = initial; + for (const step of steps) { + if (signal == null ? void 0 : signal.aborted) throw new DOMException("Aborted", "AbortError"); + if (typeof ctx === "string" || (ctx == null ? void 0 : ctx.url)) ctx = await this.request(typeof ctx === "string" ? { + url: ctx, + signal + } : {...ctx, signal}); + ctx = await step(ctx); + } + return ctx; + } + } - /** - * @connect 115vod.com - */ - static filesVideo(pickcode) { - return this.request({ - url: "https://115vod.com/webapi/files/video", - params: {pickcode, local: 1} - }); - } + __publicField(Req, "DEFAULT_GET_RESPONSE_TYPE", "document"); + __publicField(Req, "DEFAULT_POST_RESPONSE_TYPE", "json"); + __publicField(Req, "DEFAULT_TIMEOUT", 3e4); + __publicField(Req, "DEFAULT_METHOD", "GET"); + __publicField(Req, "_isRawBody", (v) => v instanceof FormData || v instanceof Blob || v instanceof ArrayBuffer || v instanceof URLSearchParams); + __publicField(Req, "_isPlainObject", (v) => Object.prototype.toString.call(v) === "[object Object]"); - static post(details) { - return this.request({method: "POST", ...details}); - } + class Drive115 extends Req { + static files(cid = "0", params = {}) { + return this.request({ + url: "https://webapi.115.com/files", + params: {cid, ...params} + }); + } - static filesAdd(cname, pid) { - return this.post({ - url: "https://webapi.115.com/files/add", - data: {cname, pid} - }); - } + static filesSearch(search_value, params = {}) { + return this.request({ + url: "https://webapi.115.com/files/search", + params: {search_value, ...params} + }); + } - static lixianAddTaskUrl(url, wp_path_id) { - return this.post({ - url: "https://115.com/web/lixian/", - params: {ct: "lixian", ac: "add_task_url"}, - data: {url, wp_path_id} - }); - } + static lixianTaskLists(page = 1) { + return this.request({ + url: "https://115.com/web/lixian/", + params: {ct: "lixian", ac: "task_lists", page} + }); + } - /** - * Bulk delete offline tasks and source files - * @param {string[]} hash Array of info_hashes - */ - static lixianTaskDel(hash) { - return this.post({ - url: "https://115.com/web/lixian/", - params: {ct: "lixian", ac: "task_del"}, - data: {hash} - }); - } + static labelList() { + return this.request({ + url: "https://webapi.115.com/label/list", + params: {keyword: "", limit: 11150} + }); + } - /** - * Bulk delete files - * @param {string[]} fid Array of file IDs - * @param {string} pid Parent folder ID - */ - static rbDelete(fid, pid = "") { - return this.post({ - url: "https://webapi.115.com/rb/delete", - data: {fid, pid, ignore_warn: 1} - }); - } + /** + * @connect 115vod.com + */ + static filesVideo(pickcode) { + return this.request({ + url: "https://115vod.com/webapi/files/video", + params: {pickcode, local: 1} + }); + } - /** - * Batch move files - * @param {string[]} fid Array of file IDs - * @param {string} pid Destination folder ID - */ - static filesMove(fid, pid) { - return this.post({ - url: "https://webapi.115.com/files/move", - data: {fid, pid, move_proid: ""} - }); - } + static post(details) { + return this.request({method: "POST", ...details}); + } - /** - * Bulk label files - * @param {string} file_ids fid1,fid2,fid3... - * @param {string} file_label label_id1,label_id2,label_id3... - */ - static filesBatchLabel(file_ids, file_label, action = "add") { - return this.post({ - url: "https://webapi.115.com/files/batch_label", - data: {file_ids, file_label, action} - }); - } + static filesAdd(cname, pid) { + return this.post({ + url: "https://webapi.115.com/files/add", + data: {cname, pid} + }); + } - /** - * Bulk rename files - * @param {object} files_new_name { [fid]: rename } - */ - static filesBatchRename(files_new_name) { - return this.post({ - url: "https://webapi.115.com/files/batch_rename", - data: {files_new_name} - }); - } + static lixianAddTaskUrl(url, wp_path_id) { + return this.post({ + url: "https://115.com/web/lixian/", + params: {ct: "lixian", ac: "add_task_url"}, + data: {url, wp_path_id} + }); + } - static sampleInitUpload({filename, filesize, cid}) { - return this.post({ - url: "https://uplb.115.com/3.0/sampleinitupload.php", - data: {filename, filesize, target: `U_1_${cid}`} - }); - } + /** + * Bulk delete offline tasks and source files + * @param {string[]} hash Array of info_hashes + */ + static lixianTaskDel(hash) { + return this.post({ + url: "https://115.com/web/lixian/", + params: {ct: "lixian", ac: "task_del"}, + data: {hash} + }); + } - /** - * @connect aliyuncs.com - */ - static upload({ - host: url, - filename: name2, - object: key, - policy, - accessid: OSSAccessKeyId, - callback, - signature, - file - }) { - return this.post({ - url, - data: { - name: name2, - key, + /** + * Bulk delete files + * @param {string[]} fid Array of file IDs + * @param {string} pid Parent folder ID + */ + static rbDelete(fid, pid = "") { + return this.post({ + url: "https://webapi.115.com/rb/delete", + data: {fid, pid, ignore_warn: 1} + }); + } + + /** + * Batch move files + * @param {string[]} fid Array of file IDs + * @param {string} pid Destination folder ID + */ + static filesMove(fid, pid) { + return this.post({ + url: "https://webapi.115.com/files/move", + data: {fid, pid, move_proid: ""} + }); + } + + /** + * Bulk label files + * @param {string} file_ids fid1,fid2,fid3... + * @param {string} file_label label_id1,label_id2,label_id3... + */ + static filesBatchLabel(file_ids, file_label, action = "add") { + return this.post({ + url: "https://webapi.115.com/files/batch_label", + data: {file_ids, file_label, action} + }); + } + + /** + * Bulk rename files + * @param {object} files_new_name { [fid]: rename } + */ + static filesBatchRename(files_new_name) { + return this.post({ + url: "https://webapi.115.com/files/batch_rename", + data: {files_new_name} + }); + } + + static sampleInitUpload({filename, filesize, cid}) { + return this.post({ + url: "https://uplb.115.com/3.0/sampleinitupload.php", + data: {filename, filesize, target: `U_1_${cid}`} + }); + } + + /** + * @connect aliyuncs.com + */ + static upload({ + host: url, + filename: name2, + object: key, policy, - OSSAccessKeyId, - success_action_status: "200", + accessid: OSSAccessKeyId, callback, signature, file - } - }); - } - - static filesEdit(fid, fid_cover) { - return this.post({ - url: "https://webapi.115.com/files/edit", - data: {fid, fid_cover} - }); + }) { + return this.post({ + url, + data: { + name: name2, + key, + policy, + OSSAccessKeyId, + success_action_status: "200", + callback, + signature, + file } + }); } - __publicField(Drive115, "DEFAULT_GET_RESPONSE_TYPE", "json"); + static filesEdit(fid, fid_cover) { + return this.post({ + url: "https://webapi.115.com/files/edit", + data: {fid, fid_cover} + }); + } + } - class Req115 extends Drive115 { - static async filesAll(cid, params = {}) { - const res = await this.files(cid, params); - const {count, page_size, data} = res; - return count > page_size && data.length ? this.files(cid, {...params, limit: count}) : res; - } + __publicField(Drive115, "DEFAULT_GET_RESPONSE_TYPE", "json"); - static filesAllVideos(cid, params = {}) { - return this.filesAll(cid, {...params, type: 4}); - } - - static filesAllSRTs(cid, params = {}) { - return this.filesAll(cid, {...params, suffix: "srt"}); - } - - static async filesSearchAll(search_value, params = {}) { - const res = await this.filesSearch(search_value, params); - const {count, page_size, data} = res; - return count > page_size && data.length ? this.filesSearch(search_value, {...params, limit: count}) : res; - } - - static filesSearchAllVideos(search_value, params = {}) { - return this.filesSearchAll(search_value, {...params, type: 4}); - } - - static filesSearchAllFolders(search_value, params = {}) { - return this.filesSearchAll(search_value, {...params, fc: 1}); - } - - static async handleDir(routes) { - var _a; - if (routes.length === 1 && /^\d{5,}$/.test(routes[0])) return routes[0]; - let cid; - const routesStr = routes.join("/"); - const cachedCid = localStorage.getItem(routesStr); - if (cachedCid) { - const res = await this.files(cachedCid); - if ((_a = res == null ? void 0 : res.path) == null ? void 0 : _a.length) { - const path = res.path.slice(1).map((p) => p.name); - if (path.join("/") === routesStr) cid = cachedCid; - } - } - if (!cid) { - cid = "0"; - for (const route of routes) { - const {data} = await this.filesSearchAllFolders(route, {cid}); - let folder = data.find((folder2) => folder2.n === route); - if (!folder) folder = await this.filesAdd(route, cid); - cid = folder == null ? void 0 : folder.cid; - if (!cid) break; - } - } - const month = (/* @__PURE__ */ new Date()).getMonth().toString(); - if (localStorage.getItem("115_CD") !== month) { - localStorage.clear(); - localStorage.setItem("115_CD", month); - } - if (cid) localStorage.setItem(routesStr, cid); - return cid; - } - - static async handleVerify(info_hash, {regex, codes}, {max, filter}) { - const sleep = () => { - return new Promise((r) => { - setTimeout(r, 1e3); - }); - }; - let file_id = ""; - let videos = []; - for (let index = 0; index < max; index++) { - if (index) await sleep(); - const {tasks} = await this.lixianTaskLists(); - const task = tasks.find((task2) => task2.info_hash === info_hash); - if (!task || task.status === -1) break; - file_id = task.file_id; - if (file_id) break; - } - if (!file_id) return {file_id, videos}; - for (let index = 0; index < max; index++) { - if (index) await sleep(); - const {data} = await this.filesAllVideos(file_id); - videos = data.filter((item) => regex.test(item.n)); - if (videos.length) break; - } - if (!videos.length) { - const {tasks} = await this.lixianTaskLists(); - const task = tasks.find((task2) => task2.info_hash === info_hash); - if (task.status === 2) { - const {data} = await this.filesAllVideos(file_id); - codes = codes.map((code) => code.toUpperCase()); - videos = data.filter((item) => { - const name2 = item.n.toUpperCase(); - return codes.some((code) => name2.includes(code)); - }); - } - } - return {videos: videos.filter(filter), file_id}; - } - - static async handleClean(keepFiles, cid) { - const needMove = keepFiles.filter((file) => file.cid !== cid).map((file) => file.fid); - if (needMove.length) await this.filesMove(needMove, cid); - const {data} = await this.filesAll(cid); - const needRemove = data.filter((item) => !keepFiles.some((file) => file.fid === item.fid)).map((item) => item.fid ?? item.cid); - if (needRemove.length) return this.rbDelete(needRemove, cid); - } - - static async handleTags(files, tags) { - var _a; - const {data} = await this.labelList(); - if (!((_a = data == null ? void 0 : data.list) == null ? void 0 : _a.length)) return; - const {list} = data; - const labels = []; - tags.forEach((tag) => { - const item = list.find((item2) => item2.name === tag); - if (item) labels.push(item.id); - }); - if (labels.length) return this.filesBatchLabel(files.map((it) => it.fid).toString(), labels.toString()); - } - - static handleRename(files, cid, {rename, renameTxt, zh, crack}) { - rename = rename.replaceAll("$zh", zh ? renameTxt.zh : ""); - rename = rename.replaceAll("$crack", crack ? renameTxt.crack : ""); - rename = rename.split("$sep").filter(Boolean).join(renameTxt.sep); - rename = rename.trim(); - const renameObj = {[cid]: rename}; - if (files.length === 1) { - const {fid, ico} = files[0]; - renameObj[fid] = `${rename}.${ico}`; - return this.filesBatchRename(renameObj); - } - const icoMap = files.reduce((acc, {ico, ...item}) => { - acc[ico] ?? (acc[ico] = []); - acc[ico].push(item); - return acc; - }, {}); - const noTxt = renameTxt.no; - for (const [ico, items] of Object.entries(icoMap)) { - if (items.length === 1) { - renameObj[items[0].fid] = `${rename}.${ico}`; - continue; - } - items.toSorted((a, b) => a.n.localeCompare(b.n)).forEach(({fid}, idx) => { - const no = noTxt.replaceAll(`\${no}`, `${idx + 1}`.padStart(2, "0")); - renameObj[fid] = `${rename}${no}.${ico}`; - }); - } - return this.filesBatchRename(renameObj); - } - - static async handleCover(url, cid, filename) { - const file = await this.request({url, timeout: 6e4, responseType: "blob"}); - if (!file) return; - const res = await this.sampleInitUpload({cid, filename, filesize: file.size}); - if (res == null ? void 0 : res.host) return this.upload({...res, filename, file}); - } - - static async handleOffline({ - dir, - regex, - codes, - verifyOptions, - code, - rename, - renameTxt, - tags, - clean, - cover - }, magnets) { - const res = {status: "error", msg: `获取目录失败: ${dir.join("/")}`}; - const cid = await this.handleDir(dir); - if (!cid) return res; - for (let index = 0, {length} = magnets; index < length; index++) { - const {url, zh, crack} = magnets[index]; - const {state, error_msg, errcode, info_hash} = await this.lixianAddTaskUrl(url, cid); - if (!state) { - res.msg = error_msg; - res.status = "error"; - res.currIdx = index; - if (errcode === 10008) continue; - if (errcode === 911) res.status = "warn"; - break; - } - const {videos, file_id} = await this.handleVerify(info_hash, {regex, codes}, verifyOptions); - if (!videos.length) { - if (verifyOptions.clean) this.lixianTaskDel([info_hash]); - if (file_id && clean) this.rbDelete([file_id], cid); - res.msg = `${code} 离线验证失败`; - res.status = "error"; - continue; - } - const {data: srts = []} = await this.filesAllSRTs(file_id); - const files = [...videos, ...srts]; - if (clean) await this.handleClean(files, file_id); - if (tags.length) this.handleTags(videos, tags); - if (rename) this.handleRename(files, file_id, {rename, renameTxt, zh: zh || srts.length, crack}); - if (cover) { - try { - const {data} = await this.handleCover(cover, file_id, `${code}.cover.jpg`); - if (data == null ? void 0 : data.file_id) this.filesEdit(file_id, data.file_id); - } catch (err) { - console.warn("[Req115.handleCover]", err == null ? void 0 : err.message); - } - } - res.msg = `${code} 离线任务成功`; - res.status = "success"; - break; - } - return res; - } - - static async queueMutation(name2, fn) { - return fn(); - } + class Req115 extends Drive115 { + static async filesAll(cid, params = {}) { + const res = await this.files(cid, params); + const {count, page_size, data} = res; + return count > page_size && data.length ? this.files(cid, {...params, limit: count}) : res; } - window.Req115 = Req115; + static filesAllVideos(cid, params = {}) { + return this.filesAll(cid, {...params, type: 4}); + } + + static filesAllSRTs(cid, params = {}) { + return this.filesAll(cid, {...params, suffix: "srt"}); + } + + static async filesSearchAll(search_value, params = {}) { + const res = await this.filesSearch(search_value, params); + const {count, page_size, data} = res; + return count > page_size && data.length ? this.filesSearch(search_value, {...params, limit: count}) : res; + } + + static filesSearchAllVideos(search_value, params = {}) { + return this.filesSearchAll(search_value, {...params, type: 4}); + } + + static filesSearchAllFolders(search_value, params = {}) { + return this.filesSearchAll(search_value, {...params, fc: 1}); + } + + static async handleDir(routes) { + var _a; + if (routes.length === 1 && /^\d{5,}$/.test(routes[0])) return routes[0]; + let cid; + const routesStr = routes.join("/"); + const cachedCid = localStorage.getItem(routesStr); + if (cachedCid) { + const res = await this.files(cachedCid); + if ((_a = res == null ? void 0 : res.path) == null ? void 0 : _a.length) { + const path = res.path.slice(1).map((p) => p.name); + if (path.join("/") === routesStr) cid = cachedCid; + } + } + if (!cid) { + cid = "0"; + for (const route of routes) { + const {data} = await this.filesSearchAllFolders(route, {cid}); + let folder = data.find((folder2) => folder2.n === route); + if (!folder) folder = await this.filesAdd(route, cid); + cid = folder == null ? void 0 : folder.cid; + if (!cid) break; + } + } + const month = (/* @__PURE__ */ new Date()).getMonth().toString(); + if (localStorage.getItem("115_CD") !== month) { + localStorage.clear(); + localStorage.setItem("115_CD", month); + } + if (cid) localStorage.setItem(routesStr, cid); + return cid; + } + + static async handleVerify(info_hash, {regex, codes}, {max, filter}) { + const sleep = () => { + return new Promise((r) => { + setTimeout(r, 1e3); + }); + }; + let file_id = ""; + let videos = []; + for (let index = 0; index < max; index++) { + if (index) await sleep(); + const {tasks} = await this.lixianTaskLists(); + const task = tasks.find((task2) => task2.info_hash === info_hash); + if (!task || task.status === -1) break; + file_id = task.file_id; + if (file_id) break; + } + if (!file_id) return {file_id, videos}; + for (let index = 0; index < max; index++) { + if (index) await sleep(); + const {data} = await this.filesAllVideos(file_id); + videos = data.filter((item) => regex.test(item.n)); + if (videos.length) break; + } + if (!videos.length) { + const {tasks} = await this.lixianTaskLists(); + const task = tasks.find((task2) => task2.info_hash === info_hash); + if (task.status === 2) { + const {data} = await this.filesAllVideos(file_id); + codes = codes.map((code) => code.toUpperCase()); + videos = data.filter((item) => { + const name2 = item.n.toUpperCase(); + return codes.some((code) => name2.includes(code)); + }); + } + } + return {videos: videos.filter(filter), file_id}; + } + + static async handleClean(keepFiles, cid) { + const needMove = keepFiles.filter((file) => file.cid !== cid).map((file) => file.fid); + if (needMove.length) await this.filesMove(needMove, cid); + const {data} = await this.filesAll(cid); + const needRemove = data.filter((item) => !keepFiles.some((file) => file.fid === item.fid)).map((item) => item.fid ?? item.cid); + if (needRemove.length) return this.rbDelete(needRemove, cid); + } + + static async handleTags(files, tags) { + var _a; + const {data} = await this.labelList(); + if (!((_a = data == null ? void 0 : data.list) == null ? void 0 : _a.length)) return; + const {list} = data; + const labels = []; + tags.forEach((tag) => { + const item = list.find((item2) => item2.name === tag); + if (item) labels.push(item.id); + }); + if (labels.length) return this.filesBatchLabel(files.map((it) => it.fid).toString(), labels.toString()); + } + + static handleRename(files, cid, {rename, renameTxt, zh, crack}) { + rename = rename.replaceAll("$zh", zh ? renameTxt.zh : ""); + rename = rename.replaceAll("$crack", crack ? renameTxt.crack : ""); + rename = rename.split("$sep").filter(Boolean).join(renameTxt.sep); + rename = rename.trim(); + const renameObj = {[cid]: rename}; + if (files.length === 1) { + const {fid, ico} = files[0]; + renameObj[fid] = `${rename}.${ico}`; + return this.filesBatchRename(renameObj); + } + const icoMap = files.reduce((acc, {ico, ...item}) => { + acc[ico] ?? (acc[ico] = []); + acc[ico].push(item); + return acc; + }, {}); + const noTxt = renameTxt.no; + for (const [ico, items] of Object.entries(icoMap)) { + if (items.length === 1) { + renameObj[items[0].fid] = `${rename}.${ico}`; + continue; + } + items.toSorted((a, b) => a.n.localeCompare(b.n)).forEach(({fid}, idx) => { + const no = noTxt.replaceAll(`\${no}`, `${idx + 1}`.padStart(2, "0")); + renameObj[fid] = `${rename}${no}.${ico}`; + }); + } + return this.filesBatchRename(renameObj); + } + + static async handleCover(url, cid, filename) { + const file = await this.request({url, timeout: 6e4, responseType: "blob"}); + if (!file) return; + const res = await this.sampleInitUpload({cid, filename, filesize: file.size}); + if (res == null ? void 0 : res.host) return this.upload({...res, filename, file}); + } + + static async handleOffline({ + dir, + regex, + codes, + verifyOptions, + code, + rename, + renameTxt, + tags, + clean, + cover + }, magnets) { + const res = {status: "error", msg: `获取目录失败: ${dir.join("/")}`}; + const cid = await this.handleDir(dir); + if (!cid) return res; + for (let index = 0, {length} = magnets; index < length; index++) { + const {url, zh, crack} = magnets[index]; + const {state, error_msg, errcode, info_hash} = await this.lixianAddTaskUrl(url, cid); + if (!state) { + res.msg = error_msg; + res.status = "error"; + res.currIdx = index; + if (errcode === 10008) continue; + if (errcode === 911) res.status = "warn"; + break; + } + const {videos, file_id} = await this.handleVerify(info_hash, {regex, codes}, verifyOptions); + if (!videos.length) { + if (verifyOptions.clean) this.lixianTaskDel([info_hash]); + if (file_id && clean) this.rbDelete([file_id], cid); + res.msg = `${code} 离线验证失败`; + res.status = "error"; + continue; + } + const {data: srts = []} = await this.filesAllSRTs(file_id); + const files = [...videos, ...srts]; + if (clean) await this.handleClean(files, file_id); + if (tags.length) this.handleTags(videos, tags); + if (rename) this.handleRename(files, file_id, {rename, renameTxt, zh: zh || srts.length, crack}); + if (cover) { + try { + const {data} = await this.handleCover(cover, file_id, `${code}.cover.jpg`); + if (data == null ? void 0 : data.file_id) this.filesEdit(file_id, data.file_id); + } catch (err) { + console.warn("[Req115.handleCover]", err == null ? void 0 : err.message); + } + } + res.msg = `${code} 离线任务成功`; + res.status = "success"; + break; + } + return res; + } + + static async queueMutation(name2, fn) { + return fn(); + } + } + + window.Req115 = Req115; class Fc2Plugin extends BasePlugin { getName() { return "Fc2Plugin"; @@ -8456,7 +8453,7 @@ ${value}\r layer.open({ type: 1, title: carNum2, - content: '\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 \n
\n ', + content: '\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 \n
\n ', area: utils.getResponsiveArea(["70%", "90%"]), skin: "movie-detail-layer", scrollbar: false, @@ -8513,10 +8510,10 @@ ${value}\r window.refresh(); layer.closeAll(); })); - $("#subtitleSearchBtn").on("click", (() => { - window.JavPackSubtitle.openSearchModal({details: {code: carNum2}}); - })); - window.JavPackSubtitle.preload115Matches(carNum2); + $("#subtitleSearchBtn").on("click", (() => { + window.JavPackSubtitle.openSearchModal({details: {code: carNum2}}); + })); + window.JavPackSubtitle.preload115Matches(carNum2); $("#magnetSearchBtn").on("click", (() => { let magnetHub = this.getBean("MagnetHubPlugin").createMagnetHub(carNum2); layer.open({ @@ -8601,7 +8598,7 @@ ${value}\r utils.loopDetector((() => $(".movie-gallery .image-list").length > 0), (async () => { $(".movie-gallery .image-list").prepend('
正在加载缩略图
'); const screenShotPlugin = this.getBean("ScreenShotPlugin"), imgUrl = await screenShotPlugin.getScreenshot(carNum2); - imgUrl && await screenShotPlugin.addImg("缩略图", imgUrl, carNum2); + imgUrl && await screenShotPlugin.addImg("缩略图", imgUrl, carNum2); })); } handleMagnets(movieId) { @@ -8668,208 +8665,208 @@ ${value}\r return "HighlightMagnetPlugin"; } - parseDateFromAttr(dateStr) { - if (!dateStr) return 0; - if (dateStr.length === 8) { - const y = parseInt(dateStr.substring(0, 4), 10); - const m = parseInt(dateStr.substring(4, 6), 10) - 1; - const d2 = parseInt(dateStr.substring(6, 8), 10); - const date = new Date(y, m, d2); - return isNaN(date.getTime()) ? 0 : date.getTime(); - } - const d = new Date(dateStr); - return isNaN(d.getTime()) ? 0 : d.getTime(); + parseDateFromAttr(dateStr) { + if (!dateStr) return 0; + if (dateStr.length === 8) { + const y = parseInt(dateStr.substring(0, 4), 10); + const m = parseInt(dateStr.substring(4, 6), 10) - 1; + const d2 = parseInt(dateStr.substring(6, 8), 10); + const date = new Date(y, m, d2); + return isNaN(date.getTime()) ? 0 : date.getTime(); } + const d = new Date(dateStr); + return isNaN(d.getTime()) ? 0 : d.getTime(); + } - parseSizeToBytes(sizeText) { - if (!sizeText) return 0; - const s = sizeText.trim().toUpperCase(); - const num = parseFloat(s); - if (isNaN(num)) return 0; - if (s.includes("TB")) return num * 1024 * 1024 * 1024 * 1024; - if (s.includes("GB")) return num * 1024 * 1024 * 1024; - if (s.includes("MB")) return num * 1024 * 1024; - if (s.includes("KB")) return num * 1024; - return num; + parseSizeToBytes(sizeText) { + if (!sizeText) return 0; + const s = sizeText.trim().toUpperCase(); + const num = parseFloat(s); + if (isNaN(num)) return 0; + if (s.includes("TB")) return num * 1024 * 1024 * 1024 * 1024; + if (s.includes("GB")) return num * 1024 * 1024 * 1024; + if (s.includes("MB")) return num * 1024 * 1024; + if (s.includes("KB")) return num * 1024; + return num; + } + + getRowInfo(row) { + const $row = $(row); + const nameText = $row.find(".name").text().toLowerCase(); + const tags = []; + $row.find(".tags .tag").each((_, tag) => tags.push($(tag).text().toLowerCase())); + const sizeMB = parseInt($row.attr("data-size"), 10) || 0; + const files = parseInt($row.attr("data-files"), 10) || 0; + const dateStr = $row.attr("data-date") || ""; + return { + row, + $row, + nameText, + date: this.parseDateFromAttr(dateStr), + dateStr, + size: sizeMB * 1024 * 1024, + files, + tags, + is4K: nameText.includes("4k") || nameText.includes("[4k]") || tags.includes("4k"), + isHD: nameText.includes("高清") || tags.includes("高清"), + isSubtitle: nameText.includes("字幕") || nameText.includes("-c") || nameText.includes(".chs") || nameText.includes(".cht") || nameText.includes(".chi") || tags.includes("字幕"), + isUncensored: nameText.includes("无码") || nameText.includes("無碼") || nameText.includes("-u") || nameText.includes("-uc") || tags.includes("无码") || tags.includes("無碼") + }; + } + + getRowInfoForBus(row) { + const $row = $(row); + const $nameCell = $row.find("td:first-child"); + const nameText = ($nameCell.find("a:first-child").text() || "").toLowerCase().trim(); + const sizeText = ($row.find("td:nth-child(2)").text() || "").trim(); + const dateText = ($row.find("td:nth-child(3)").text() || "").trim(); + const hasHDButton = $nameCell.find("a.btn-primary").text().includes("高清"); + return { + row, + $row, + nameText, + date: this.parseDateFromAttr(dateText), + dateStr: dateText, + size: this.parseSizeToBytes(sizeText), + files: 0, + tags: [], + is4K: nameText.includes("4k") || nameText.includes("[4k]"), + isHD: hasHDButton || nameText.includes("高清"), + isSubtitle: nameText.includes("字幕") || nameText.includes("-c") || nameText.includes(".chs") || nameText.includes(".cht") || nameText.includes(".chi"), + isUncensored: nameText.includes("无码") || nameText.includes("無碼") || nameText.includes("-u") || nameText.includes("-uc") + }; + } + + async applySortAndFilter(settings) { + if (isJavBus$1) { + return this.applySortAndFilterForBus(settings); } - - getRowInfo(row) { - const $row = $(row); - const nameText = $row.find(".name").text().toLowerCase(); - const tags = []; - $row.find(".tags .tag").each((_, tag) => tags.push($(tag).text().toLowerCase())); - const sizeMB = parseInt($row.attr("data-size"), 10) || 0; - const files = parseInt($row.attr("data-files"), 10) || 0; - const dateStr = $row.attr("data-date") || ""; - return { - row, - $row, - nameText, - date: this.parseDateFromAttr(dateStr), - dateStr, - size: sizeMB * 1024 * 1024, - files, - tags, - is4K: nameText.includes("4k") || nameText.includes("[4k]") || tags.includes("4k"), - isHD: nameText.includes("高清") || tags.includes("高清"), - isSubtitle: nameText.includes("字幕") || nameText.includes("-c") || nameText.includes(".chs") || nameText.includes(".cht") || nameText.includes(".chi") || tags.includes("字幕"), - isUncensored: nameText.includes("无码") || nameText.includes("無碼") || nameText.includes("-u") || nameText.includes("-uc") || tags.includes("无码") || tags.includes("無碼") - }; - } - - getRowInfoForBus(row) { - const $row = $(row); - const $nameCell = $row.find("td:first-child"); - const nameText = ($nameCell.find("a:first-child").text() || "").toLowerCase().trim(); - const sizeText = ($row.find("td:nth-child(2)").text() || "").trim(); - const dateText = ($row.find("td:nth-child(3)").text() || "").trim(); - const hasHDButton = $nameCell.find("a.btn-primary").text().includes("高清"); - return { - row, - $row, - nameText, - date: this.parseDateFromAttr(dateText), - dateStr: dateText, - size: this.parseSizeToBytes(sizeText), - files: 0, - tags: [], - is4K: nameText.includes("4k") || nameText.includes("[4k]"), - isHD: hasHDButton || nameText.includes("高清"), - isSubtitle: nameText.includes("字幕") || nameText.includes("-c") || nameText.includes(".chs") || nameText.includes(".cht") || nameText.includes(".chi"), - isUncensored: nameText.includes("无码") || nameText.includes("無碼") || nameText.includes("-u") || nameText.includes("-uc") - }; - } - - async applySortAndFilter(settings) { - if (isJavBus$1) { - return this.applySortAndFilterForBus(settings); - } if (!isJavDb$1) return; - const $container = $("#magnets-content"); - const $rows = $container.find(".item[data-rank]"); - if (0 === $rows.length) return; - const rowInfos = $rows.toArray().map((row) => this.getRowInfo(row)); - const {filterHD, filter4K, filterSubtitle, filterUncensored, sortOrder} = settings; - const anyFilterEnabled = filterHD || filter4K || filterSubtitle || filterUncensored; - let visibleInfos = rowInfos; - if (anyFilterEnabled) { - visibleInfos = rowInfos.filter((info) => { - if (filterHD && info.isHD) return true; - if (filter4K && info.is4K) return true; - if (filterSubtitle && info.isSubtitle) return true; - if (filterUncensored && info.isUncensored) return true; - return false; - }); - } - if (sortOrder && sortOrder.length > 0) { - visibleInfos.sort((a, b) => { - for (const key of sortOrder) { - let cmp = 0; - switch (key) { - case "date": - cmp = b.date - a.date; - break; - case "size": - cmp = b.size - a.size; - break; - case "files": - cmp = a.files - b.files; - break; - } - if (0 !== cmp) return cmp; - } - return 0; - }); - } - const container = document.getElementById("magnets-content"); - if (!container) return; - const hiddenInfos = rowInfos.filter((info) => !visibleInfos.includes(info)); - const reviewsContainer = document.getElementById("reviewsContainer"); - const insertRef = reviewsContainer && reviewsContainer.parentNode === container ? reviewsContainer.previousElementSibling || reviewsContainer : null; - const fragment = document.createDocumentFragment(); - visibleInfos.forEach((info) => fragment.appendChild(info.row)); - hiddenInfos.forEach((info) => fragment.appendChild(info.row)); - if (insertRef) { - container.insertBefore(fragment, insertRef); - } else { - container.appendChild(fragment); - } - visibleInfos.forEach((info) => { - if (info.is4K) { - info.$row.addClass("magnet-4k-highlight"); - } - }); - hiddenInfos.forEach((info) => $(info.row).hide()); + const $container = $("#magnets-content"); + const $rows = $container.find(".item[data-rank]"); + if (0 === $rows.length) return; + const rowInfos = $rows.toArray().map((row) => this.getRowInfo(row)); + const {filterHD, filter4K, filterSubtitle, filterUncensored, sortOrder} = settings; + const anyFilterEnabled = filterHD || filter4K || filterSubtitle || filterUncensored; + let visibleInfos = rowInfos; + if (anyFilterEnabled) { + visibleInfos = rowInfos.filter((info) => { + if (filterHD && info.isHD) return true; + if (filter4K && info.is4K) return true; + if (filterSubtitle && info.isSubtitle) return true; + if (filterUncensored && info.isUncensored) return true; + return false; + }); } - - applySortAndFilterForBus(settings) { - const $table = $("#magnet-table"); - if (!$table.length) return; - const allRows = $table.find("tr").toArray(); - const headerRow = allRows.find((r) => $(r).find("td").length === 0 || $(r).css("font-weight") === "bold" || $(r).css("fontWeight") === "bold" || $(r).css("font-weight") === "700"); - const dataRows = headerRow ? allRows.filter((r) => r !== headerRow) : allRows.slice(1); - if (0 === dataRows.length) return; - const rowInfos = dataRows.map((row) => this.getRowInfoForBus(row)); - const {filterHD, filter4K, filterSubtitle, filterUncensored, sortOrder} = settings; - const anyFilterEnabled = filterHD || filter4K || filterSubtitle || filterUncensored; - let visibleInfos = rowInfos; - if (anyFilterEnabled) { - visibleInfos = rowInfos.filter((info) => { - if (filterHD && info.isHD) return true; - if (filter4K && info.is4K) return true; - if (filterSubtitle && info.isSubtitle) return true; - if (filterUncensored && info.isUncensored) return true; - return false; - }); + if (sortOrder && sortOrder.length > 0) { + visibleInfos.sort((a, b) => { + for (const key of sortOrder) { + let cmp = 0; + switch (key) { + case "date": + cmp = b.date - a.date; + break; + case "size": + cmp = b.size - a.size; + break; + case "files": + cmp = a.files - b.files; + break; + } + if (0 !== cmp) return cmp; } - if (sortOrder && sortOrder.length > 0) { - visibleInfos.sort((a, b) => { - for (const key of sortOrder) { - let cmp = 0; - switch (key) { - case "date": - cmp = b.date - a.date; - break; - case "size": - cmp = b.size - a.size; - break; - } - if (0 !== cmp) return cmp; - } - return 0; - }); - } - const hiddenInfos = rowInfos.filter((info) => !visibleInfos.includes(info)); - const fragment = document.createDocumentFragment(); - visibleInfos.forEach((info) => fragment.appendChild(info.row)); - hiddenInfos.forEach((info) => fragment.appendChild(info.row)); - const insertAnchor = headerRow || $table.find("tr").first()[0]; - if (insertAnchor && insertAnchor.nextSibling) { - $(insertAnchor).after(fragment); - } else { - $table.append(fragment); - } - visibleInfos.forEach((info) => { - if (info.is4K) { - info.$row.addClass("magnet-4k-highlight"); - } - }); - hiddenInfos.forEach((info) => $(info.row).hide()); + return 0; + }); } + const container = document.getElementById("magnets-content"); + if (!container) return; + const hiddenInfos = rowInfos.filter((info) => !visibleInfos.includes(info)); + const reviewsContainer = document.getElementById("reviewsContainer"); + const insertRef = reviewsContainer && reviewsContainer.parentNode === container ? reviewsContainer.previousElementSibling || reviewsContainer : null; + const fragment = document.createDocumentFragment(); + visibleInfos.forEach((info) => fragment.appendChild(info.row)); + hiddenInfos.forEach((info) => fragment.appendChild(info.row)); + if (insertRef) { + container.insertBefore(fragment, insertRef); + } else { + container.appendChild(fragment); + } + visibleInfos.forEach((info) => { + if (info.is4K) { + info.$row.addClass("magnet-4k-highlight"); + } + }); + hiddenInfos.forEach((info) => $(info.row).hide()); + } - showAll() { - if (isJavDb$1) { - $("#magnets-content .item[data-rank]").toArray().forEach(((el) => { - $(el).show(); - $(el).removeClass("magnet-4k-highlight"); - })); - } - if (isJavBus$1) { - $("#magnet-table tr").toArray().forEach(((el) => { - $(el).show(); - $(el).removeClass("magnet-4k-highlight"); - })); + applySortAndFilterForBus(settings) { + const $table = $("#magnet-table"); + if (!$table.length) return; + const allRows = $table.find("tr").toArray(); + const headerRow = allRows.find((r) => $(r).find("td").length === 0 || $(r).css("font-weight") === "bold" || $(r).css("fontWeight") === "bold" || $(r).css("font-weight") === "700"); + const dataRows = headerRow ? allRows.filter((r) => r !== headerRow) : allRows.slice(1); + if (0 === dataRows.length) return; + const rowInfos = dataRows.map((row) => this.getRowInfoForBus(row)); + const {filterHD, filter4K, filterSubtitle, filterUncensored, sortOrder} = settings; + const anyFilterEnabled = filterHD || filter4K || filterSubtitle || filterUncensored; + let visibleInfos = rowInfos; + if (anyFilterEnabled) { + visibleInfos = rowInfos.filter((info) => { + if (filterHD && info.isHD) return true; + if (filter4K && info.is4K) return true; + if (filterSubtitle && info.isSubtitle) return true; + if (filterUncensored && info.isUncensored) return true; + return false; + }); + } + if (sortOrder && sortOrder.length > 0) { + visibleInfos.sort((a, b) => { + for (const key of sortOrder) { + let cmp = 0; + switch (key) { + case "date": + cmp = b.date - a.date; + break; + case "size": + cmp = b.size - a.size; + break; + } + if (0 !== cmp) return cmp; } + return 0; + }); + } + const hiddenInfos = rowInfos.filter((info) => !visibleInfos.includes(info)); + const fragment = document.createDocumentFragment(); + visibleInfos.forEach((info) => fragment.appendChild(info.row)); + hiddenInfos.forEach((info) => fragment.appendChild(info.row)); + const insertAnchor = headerRow || $table.find("tr").first()[0]; + if (insertAnchor && insertAnchor.nextSibling) { + $(insertAnchor).after(fragment); + } else { + $table.append(fragment); + } + visibleInfos.forEach((info) => { + if (info.is4K) { + info.$row.addClass("magnet-4k-highlight"); + } + }); + hiddenInfos.forEach((info) => $(info.row).hide()); + } + + showAll() { + if (isJavDb$1) { + $("#magnets-content .item[data-rank]").toArray().forEach(((el) => { + $(el).show(); + $(el).removeClass("magnet-4k-highlight"); + })); + } + if (isJavBus$1) { + $("#magnet-table tr").toArray().forEach(((el) => { + $(el).show(); + $(el).removeClass("magnet-4k-highlight"); + })); + } } handleBus() { isJavBus$1 && window.isDetailPage && utils.loopDetector((() => $("#magnet-table td a").length > 0), (() => { @@ -9015,7 +9012,7 @@ ${value}\r class ActressInfoPlugin extends BasePlugin { constructor() { super(...arguments); - __publicField2(this, "apiUrl", "https://ja.wikipedia.org/wiki/"); + __publicField2(this, "apiUrl", "https://ja.wikipedia.org/wiki/"); } getName() { return "ActressInfoPlugin"; @@ -9149,7 +9146,7 @@ ${value}\r class HitShowPlugin extends BasePlugin { constructor() { super(); - __publicField2(this, "$contentBox", $(".section .container")); + __publicField2(this, "$contentBox", $(".section .container")); } getName() { return "HitShowPlugin"; @@ -9251,9 +9248,9 @@ ${err.stack}` : ""); class TOP250Plugin extends BasePlugin { constructor() { super(); - __publicField2(this, "has_cnsub", ""); - __publicField2(this, "$contentBox", $(".section .container")); - __publicField2(this, "movies", []); + __publicField2(this, "has_cnsub", ""); + __publicField2(this, "$contentBox", $(".section .container")); + __publicField2(this, "movies", []); } getName() { return "TOP250Plugin"; @@ -9659,10 +9656,10 @@ ${err.stack}` : ""); } closeVideoModal() { const $previewVideo = $("#preview-video"); - if ($previewVideo.length > 0) { - const videoEl = $previewVideo[0]; - videoEl.pause(); - } + if ($previewVideo.length > 0) { + const videoEl = $previewVideo[0]; + videoEl.pause(); + } $("#bus-preview-modal").removeClass("is-open"); } async handle() { @@ -9701,8 +9698,8 @@ ${err.stack}` : ""); let $previewVideo = $("#preview-video"); if ($previewVideo.length > 0) { $modal.addClass("is-open"); - const videoEl = $previewVideo[0]; - videoEl.play().catch(((e) => console.warn("尝试播放失败 (可能被浏览器阻止):", e))); + const videoEl = $previewVideo[0]; + videoEl.play().catch(((e) => console.warn("尝试播放失败 (可能被浏览器阻止):", e))); return; } let carNum2 = this.getPageInfo().carNum; @@ -9729,10 +9726,10 @@ ${err.stack}` : "");
`); - const $videoEl = $("#preview-video"), $qualityControlsBox = $container.find(".video-control-box"); - if (!$videoEl.length) return; - const videoEl = $videoEl[0]; - const jhs_videoMuted = localStorage.getItem("jhs_videoMuted"); + const $videoEl = $("#preview-video"), $qualityControlsBox = $container.find(".video-control-box"); + if (!$videoEl.length) return; + const videoEl = $videoEl[0]; + const jhs_videoMuted = localStorage.getItem("jhs_videoMuted"); videoEl.muted = !jhs_videoMuted || "yes" === jhs_videoMuted; videoEl.addEventListener("volumechange", (function() { localStorage.setItem("jhs_videoMuted", videoEl.muted ? "yes" : "no"); @@ -9759,11 +9756,11 @@ ${err.stack}` : ""); if ($button.hasClass("active")) return; let videoSrc = $button.attr("data-video-src"); const currentTime = videoEl.currentTime; - videoEl.src = videoSrc; + videoEl.src = videoSrc; videoEl.load(); - if (currentTime > 0 && Number.isFinite(currentTime)) { - videoEl.currentTime = currentTime; - } + if (currentTime > 0 && Number.isFinite(currentTime)) { + videoEl.currentTime = currentTime; + } await videoEl.play(); $buttons.removeClass("active"); $button.addClass("active"); @@ -9774,456 +9771,456 @@ ${err.stack}` : ""); } } - const HLS_SCRIPT_URL = "https://cdn.jsdelivr.net/npm/hls.js@1.5.18/dist/hls.min.js"; - let hlsLoadPromise = null; - const getHlsClass = () => { - const HlsClass = window.Hls || globalThis.Hls || (typeof Hls !== "undefined" ? Hls : null); - return HlsClass && HlsClass.isSupported && HlsClass.isSupported() ? HlsClass : null; - }; - const binaryTextToArrayBuffer = (value) => { - const text = String(value || ""); - const bytes = new Uint8Array(text.length); - for (let index = 0; index < text.length; index += 1) { - bytes[index] = text.charCodeAt(index) & 255; - } - return bytes.buffer; - }; - const loadHlsLibrary = () => { - const readyHls = getHlsClass(); - if (readyHls) return Promise.resolve(readyHls); - if (hlsLoadPromise) return hlsLoadPromise; - const loadByGm = () => new Promise((resolve) => { - _GM_xmlhttpRequest({ - method: "GET", - url: HLS_SCRIPT_URL, - timeout: 15e3, - onload: (r) => { - if (r.status >= 200 && r.status < 300 && r.responseText) { - try { - Function(`${r.responseText} + const HLS_SCRIPT_URL = "https://cdn.jsdelivr.net/npm/hls.js@1.5.18/dist/hls.min.js"; + let hlsLoadPromise = null; + const getHlsClass = () => { + const HlsClass = window.Hls || globalThis.Hls || (typeof Hls !== "undefined" ? Hls : null); + return HlsClass && HlsClass.isSupported && HlsClass.isSupported() ? HlsClass : null; + }; + const binaryTextToArrayBuffer = (value) => { + const text = String(value || ""); + const bytes = new Uint8Array(text.length); + for (let index = 0; index < text.length; index += 1) { + bytes[index] = text.charCodeAt(index) & 255; + } + return bytes.buffer; + }; + const loadHlsLibrary = () => { + const readyHls = getHlsClass(); + if (readyHls) return Promise.resolve(readyHls); + if (hlsLoadPromise) return hlsLoadPromise; + const loadByGm = () => new Promise((resolve) => { + _GM_xmlhttpRequest({ + method: "GET", + url: HLS_SCRIPT_URL, + timeout: 15e3, + onload: (r) => { + if (r.status >= 200 && r.status < 300 && r.responseText) { + try { + Function(`${r.responseText} //# sourceURL=${HLS_SCRIPT_URL}`).call(globalThis); - } catch (err) { - clog.error("HLS: hls.js 执行失败", err); - } - } - resolve(getHlsClass()); - }, - onerror: () => resolve(getHlsClass()), - ontimeout: () => resolve(getHlsClass()) - }); - }); - hlsLoadPromise = new Promise((resolve) => { - const existing = document.querySelector('script[data-jhs-hls="1"]'); - if (existing) { - existing.addEventListener("load", () => resolve(getHlsClass()), {once: true}); - existing.addEventListener("error", () => loadByGm().then(resolve), {once: true}); - setTimeout(() => { - if (!getHlsClass()) loadByGm().then(resolve); - }, 4e3); - return; - } - const hlsScript = document.createElement("script"); - hlsScript.src = HLS_SCRIPT_URL; - hlsScript.async = true; - hlsScript.dataset.jhsHls = "1"; - hlsScript.onload = () => resolve(getHlsClass()); - hlsScript.onerror = () => loadByGm().then(resolve); - document.head.appendChild(hlsScript); - }).then((HlsClass) => { - if (!HlsClass) hlsLoadPromise = null; - return HlsClass; - }); - return hlsLoadPromise; - }; - const createHlsLoader = () => { - class GMHlsLoader { - constructor(config) { - this.config = config; - this.context = null; - this.callbacks = null; - this.loader = null; - this.stats = this.createStats(); + } catch (err) { + clog.error("HLS: hls.js 执行失败", err); } + } + resolve(getHlsClass()); + }, + onerror: () => resolve(getHlsClass()), + ontimeout: () => resolve(getHlsClass()) + }); + }); + hlsLoadPromise = new Promise((resolve) => { + const existing = document.querySelector('script[data-jhs-hls="1"]'); + if (existing) { + existing.addEventListener("load", () => resolve(getHlsClass()), {once: true}); + existing.addEventListener("error", () => loadByGm().then(resolve), {once: true}); + setTimeout(() => { + if (!getHlsClass()) loadByGm().then(resolve); + }, 4e3); + return; + } + const hlsScript = document.createElement("script"); + hlsScript.src = HLS_SCRIPT_URL; + hlsScript.async = true; + hlsScript.dataset.jhsHls = "1"; + hlsScript.onload = () => resolve(getHlsClass()); + hlsScript.onerror = () => loadByGm().then(resolve); + document.head.appendChild(hlsScript); + }).then((HlsClass) => { + if (!HlsClass) hlsLoadPromise = null; + return HlsClass; + }); + return hlsLoadPromise; + }; + const createHlsLoader = () => { + class GMHlsLoader { + constructor(config) { + this.config = config; + this.context = null; + this.callbacks = null; + this.loader = null; + this.stats = this.createStats(); + } - createStats() { - return { - aborted: false, - loaded: 0, - retry: 0, - total: 0, - chunkCount: 0, - bwEstimate: 0, - trequest: 0, - tfirst: 0, - tload: 0, - loading: {start: 0, first: 0, end: 0}, - parsing: {start: 0, end: 0}, - buffering: {start: 0, first: 0, end: 0} - }; - } + createStats() { + return { + aborted: false, + loaded: 0, + retry: 0, + total: 0, + chunkCount: 0, + bwEstimate: 0, + trequest: 0, + tfirst: 0, + tload: 0, + loading: {start: 0, first: 0, end: 0}, + parsing: {start: 0, end: 0}, + buffering: {start: 0, first: 0, end: 0} + }; + } - destroy() { - this.abort(); - } + destroy() { + this.abort(); + } - abort() { - if (this.stats) this.stats.aborted = true; - if (this.loader) { - try { - this.loader.abort(); - } catch (e) { - } - } - this.loader = null; - } + abort() { + if (this.stats) this.stats.aborted = true; + if (this.loader) { + try { + this.loader.abort(); + } catch (e) { + } + } + this.loader = null; + } - load(context, config, callbacks) { - this.context = context; - this.callbacks = callbacks; - const requestUrl = context.url; - const wantsArrayBuffer = context.responseType === "arraybuffer" || /\.(?:ts|m4s|mp4|key)(?:[?#]|$)/i.test(requestUrl); - const startedAt = performance.now(); - const stats = this.stats = this.createStats(); - stats.trequest = startedAt; - stats.tfirst = startedAt; - stats.tload = startedAt; - stats.loading.start = startedAt; - this.loader = _GM_xmlhttpRequest({ - method: "GET", - url: requestUrl, - responseType: "text", - overrideMimeType: wantsArrayBuffer ? "text/plain; charset=x-user-defined" : void 0, - timeout: config && config.timeout ? config.timeout : 2e4, - headers: { - Accept: wantsArrayBuffer ? "*/*" : "application/vnd.apple.mpegurl, application/x-mpegURL, */*" - }, - onprogress: (event) => { - stats.loaded = Number(event && event.loaded ? event.loaded : stats.loaded || 0); - stats.total = Number(event && event.total ? event.total : stats.total || stats.loaded || 0); - if (!stats.loading.first && stats.loaded > 0) { - stats.loading.first = performance.now(); - } - }, - onload: (r) => { - const status = Number(r.status || 0); - const response = { - code: status, - text: r.statusText || "", - url: r.finalUrl || requestUrl - }; - stats.tfirst = stats.tfirst || performance.now(); - stats.tload = performance.now(); - stats.loading.first = stats.loading.first || stats.tload; - stats.loading.end = stats.tload; - if (status < 200 || status >= 300) { - callbacks.onError && callbacks.onError(response, context, null, stats); - return; - } - const responseText = r.responseText || r.response || ""; - const data = wantsArrayBuffer ? binaryTextToArrayBuffer(responseText) : responseText; - stats.loaded = data && data.byteLength ? data.byteLength : data && data.length ? data.length : stats.loaded || 0; - stats.total = stats.total || stats.loaded; - stats.bwEstimate = stats.loading.end > stats.loading.first ? Math.round(stats.total * 8e3 / (stats.loading.end - stats.loading.first)) : 0; - callbacks.onSuccess && callbacks.onSuccess({data, url: response.url}, stats, context, response); - }, - onerror: () => { - callbacks.onError && callbacks.onError({ - code: 0, - text: "network error", - url: requestUrl - }, context, null, stats); - }, - ontimeout: () => { - stats.tload = performance.now(); - stats.loading.end = stats.tload; - callbacks.onTimeout && callbacks.onTimeout(stats, context, null); - } - }); + load(context, config, callbacks) { + this.context = context; + this.callbacks = callbacks; + const requestUrl = context.url; + const wantsArrayBuffer = context.responseType === "arraybuffer" || /\.(?:ts|m4s|mp4|key)(?:[?#]|$)/i.test(requestUrl); + const startedAt = performance.now(); + const stats = this.stats = this.createStats(); + stats.trequest = startedAt; + stats.tfirst = startedAt; + stats.tload = startedAt; + stats.loading.start = startedAt; + this.loader = _GM_xmlhttpRequest({ + method: "GET", + url: requestUrl, + responseType: "text", + overrideMimeType: wantsArrayBuffer ? "text/plain; charset=x-user-defined" : void 0, + timeout: config && config.timeout ? config.timeout : 2e4, + headers: { + Accept: wantsArrayBuffer ? "*/*" : "application/vnd.apple.mpegurl, application/x-mpegURL, */*" + }, + onprogress: (event) => { + stats.loaded = Number(event && event.loaded ? event.loaded : stats.loaded || 0); + stats.total = Number(event && event.total ? event.total : stats.total || stats.loaded || 0); + if (!stats.loading.first && stats.loaded > 0) { + stats.loading.first = performance.now(); } - } - - return GMHlsLoader; - }; - const isM3U8Url = (url) => /\.m3u8(?:[?#].*)?$/i.test(String(url || "")); - const attachHlsToVideo = (videoEl, src) => { - return new Promise((resolve, reject) => { - if (!videoEl || !src) { - reject(new Error("video element or source missing")); - return; - } - const HlsClass = getHlsClass(); - if (!HlsClass) { - videoEl.src = src; - videoEl.load && videoEl.load(); - resolve(false); - return; - } - const hls = new HlsClass({ - enableWorker: false, - lowLatencyMode: true, - loader: createHlsLoader(), - autoStartLoad: true, - startPosition: 0, - capLevelToPlayerSize: true, - testBandwidth: false, - preferManagedMediaSource: false, - maxBufferLength: 6, - maxMaxBufferLength: 12, - backBufferLength: 30, - maxBufferHole: 0.5, - nudgeOffset: 0.1, - manifestLoadingMaxRetry: 2, - levelLoadingMaxRetry: 2, - fragLoadingMaxRetry: 2, - manifestLoadingTimeOut: 12e3, - levelLoadingTimeOut: 12e3, - fragLoadingTimeOut: 12e3, - abrEwmaFastLive: 3, - abrEwmaSlowLive: 9 - }); - hls.on(HlsClass.Events.MANIFEST_PARSED, () => { - hls.startLoad(0); - videoEl.play().catch(() => { - }); - resolve(true); - }); - hls.on(HlsClass.Events.ERROR, (_, data) => { - if (!data || !data.fatal) return; - clog.error("HLS: 播放失败", data); - if (data.type === HlsClass.ErrorTypes.MEDIA_ERROR) { - if (videoEl.readyState >= 2) { - try { - hls.recoverMediaError && hls.recoverMediaError(); - } catch (e) { - } - return; - } - } - reject(new Error(data.details || "HLS fatal error")); - }); - hls.loadSource(src); - hls.attachMedia(videoEl); - videoEl._hls = hls; - }); - }; - const destroyHls = (videoEl) => { - if (videoEl && videoEl._hls) { - try { - videoEl._hls.destroy(); - } catch (e) { - } - videoEl._hls = null; - } - }; - const JAVXY_TOKEN = [118, 119, 112, 71, 97, 110, 28, 84, 124, 65, 76, 102, 65, 16, 77, 109, 64, 82, 85, 83, 67, 92, 125, 108, 83, 65, 124, 107, 84, 104, 71, 84, 17, 124, 118, 125, 104, 8, 125, 96, 112, 103, 29, 18, 82, 83, 87, 84].map((v) => String.fromCharCode(v ^ 37)).join(""); - const JAVXY_ENDPOINTS = [ - {host: String.fromCharCode(106, 97, 118, 120, 121, 46, 99, 99, 46, 99, 100), label: "Javxy"}, - { - host: String.fromCharCode(119, 111, 114, 107, 101, 114, 46, 106, 97, 118, 120, 121, 46, 99, 99, 46, 99, 100), - label: "Javxy Worker" - } - ]; - const JAVXY_SOURCE_LABELS = { - "Tokyo-Hot": "Javxy | Tokyo-Hot", - FC2: "Javxy | FC2", - Direct: "Javxy | Direct", - DMM: "Javxy | dmm", - MGStage: "Javxy | MGStage", - DUGA: "Javxy | DUGA", - MYWIFE: "Javxy | MyWife", - JavTrailers: "Javxy | JavTrailers", - JavDB: "Javxy | Javdb", - AVWikiDB: "Javxy | AVWikiDB", - JAVDatabase: "Javxy | JAVDatabase", - HEYZO: "Javxy | Heyzo", - HeyDouga: "Javxy | HeyDouga", - PACO: "Javxy | Paco", - "10MU": "Javxy | 10mu", - Caribbean: "Javxy | 加勒比", - "1Pondo": "Javxy | 一本道" - }; - const JAVXY_QUALITY_OPTIONS = [ - {quality: "4k", text: "4K"}, - {quality: "hhb", text: "1080p"}, - {quality: "1080p", text: "1080p"}, - {quality: "hmb", text: "720p"}, - {quality: "720p", text: "720p"}, - {quality: "mhb", text: "576p"}, - {quality: "540p", text: "540p"}, - {quality: "mmb", text: "432p"}, - {quality: "480p", text: "480p"}, - {quality: "396p", text: "396p"}, - {quality: "360p", text: "360p"}, - {quality: "240p", text: "240p"} - ]; - const selectHighestQuality = (qualityMap) => { - const rank = new Map(JAVXY_QUALITY_OPTIONS.map((item, index) => [item.quality, index])); - return Object.keys(qualityMap || {}).filter((key) => qualityMap[key]).sort((a, b) => (rank.get(a) ?? -1) - (rank.get(b) ?? -1))[0] || null; - }; - const sortQualityKeys = (qualityMap) => { - const rank = new Map(JAVXY_QUALITY_OPTIONS.map((item, index) => [item.quality, index])); - return Object.keys(qualityMap || {}).filter((key) => qualityMap[key]).sort((a, b) => (rank.get(a) ?? -1) - (rank.get(b) ?? -1)); - }; - const normalizeJavxySource = (value) => { - const raw = String(value || "").trim().toLowerCase(); - if (raw.includes("fc2")) return "FC2"; - if (raw.includes("mgstage")) return "MGStage"; - if (raw.includes("heydouga")) return "Direct"; - if (raw.includes("mywife")) return "MYWIFE"; - if (raw.includes("duga")) return "DUGA"; - if (raw.includes("javtrailers")) return "JavTrailers"; - if (raw.includes("javdb")) return "JavDB"; - if (raw.includes("avwikidb")) return "AVWikiDB"; - if (raw.includes("javdatabase")) return "JAVDatabase"; - if (raw.includes("dmm")) return "DMM"; - if (raw === "direct" || raw.includes("heyzo") || raw.includes("heydouga") || raw.includes("paco") || raw.includes("10musume") || raw.includes("10mu") || raw.includes("1pondo") || raw.includes("caribbean") || raw.includes("tokyo-hot") || raw.includes("tokyohot")) return "Direct"; - return String(value || "").trim(); - }; - const javxyQualityMapToDmmFormat = (javxyQualityMap) => { - const result = {}; - const qualityKeys = qualityOptions.map((o) => o.quality); - for (const [key, url] of Object.entries(javxyQualityMap || {})) { - if (!url) continue; - const lowerKey = key.toLowerCase(); - if (qualityKeys.includes(key)) { - result[key] = url; - } else if (qualityKeys.includes(lowerKey)) { - result[lowerKey] = url; - } - } - return result; - }; - const fromJavxyCcCd = async (id, rawCode = "", options = {}) => { - const query = String(id || rawCode || "").trim(); - if (!query) { - clog.debug("Javxy 跳过:查询词为空"); - return null; - } - for (const endpoint of JAVXY_ENDPOINTS) { - const params = new URLSearchParams({client: "laosiji-new"}); - if (Array.isArray(options.skip) && options.skip.length) params.set("skip", options.skip.join(",")); - if (Array.isArray(options.prefer) && options.prefer.length) params.set("prefer", options.prefer.join(",")); - if (Array.isArray(options.source) && options.source.length) params.set("source", options.source.join(",")); - if (options.playbackFallback) params.set("purpose", "playback-fallback"); - const apiUrl2 = `https://${endpoint.host}/trailers/${encodeURIComponent(query)}?${params}`; - clog.debug("Javxy 请求 API", {query, apiUrl: apiUrl2, endpoint: endpoint.label}); - let r; - try { - r = await gmHttp.get(apiUrl2, null, { - "Accept": "application/json,text/plain,*/*", - "X-Javxy-Token": JAVXY_TOKEN - }, {timeout: 8e3}); - } catch (e) { - clog.debug("Javxy API 网络失败,尝试下一个节点", {endpoint: endpoint.label, error: e.message}); - continue; - } - if (!r) { - clog.debug("Javxy API 无响应,尝试下一个节点", {endpoint: endpoint.label}); - continue; - } - const trailerUrl = String((r == null ? void 0 : r.trailer) || "").trim(); - if (!trailerUrl) { - clog.debug("Javxy 无 trailer 字段", {endpoint: endpoint.label, keys: Object.keys(r || {})}); - return null; - } - const qualityMap = (r == null ? void 0 : r.qualities) && typeof r.qualities === "object" ? r.qualities : {}; - const quality = (r == null ? void 0 : r.quality) && qualityMap[r.quality] ? r.quality : selectHighestQuality(qualityMap); - const sourceBase = JAVXY_SOURCE_LABELS[r == null ? void 0 : r.source] || `Javxy | ${(r == null ? void 0 : r.source) || "dmm"}`; - const directUrl = qualityMap[quality] || trailerUrl; - clog.debug("Javxy 返回结果", { - endpoint: endpoint.label, - source: r == null ? void 0 : r.source, - quality, - qualities: Object.keys(qualityMap), - url: directUrl - }); - const dmmQualityMap = javxyQualityMapToDmmFormat(qualityMap); - return { - url: directUrl, - source: sourceBase, - type: String((r == null ? void 0 : r.type) || "").trim() || "video", - qualities: qualityMap, - quality, - directUrl, - code: id, - rawCode, - javxySource: String((r == null ? void 0 : r.source) || "").trim(), - requiresJP: Boolean(r == null ? void 0 : r.requiresJP), - urls: Array.isArray(r == null ? void 0 : r.urls) && r.urls.length ? r.urls : sortQualityKeys(qualityMap).map((key) => qualityMap[key]), - dmmQualityMap + }, + onload: (r) => { + const status = Number(r.status || 0); + const response = { + code: status, + text: r.statusText || "", + url: r.finalUrl || requestUrl }; - } - return null; - }; - const fallbackJavxyResult = async (code, rawCode = "", failedSources = [], options = {}) => { - const skip = [...new Set((failedSources || []).map((source2) => normalizeJavxySource(source2)).filter(Boolean))]; - const source = [...new Set((options.source || []).map((s) => String(s || "").trim()).filter(Boolean))]; - if (!skip.length && !source.length) return null; - clog.debug("Javxy 播放失败回落查询", {code, skip, source}); - return fromJavxyCcCd(code, rawCode, {skip, source, playbackFallback: true}); - }; - const getJavxyVideoUrls = async (code, failedSources = []) => { - const result = await fallbackJavxyResult(code, code, failedSources, { - source: ["JavTrailers", "JavDB"] + stats.tfirst = stats.tfirst || performance.now(); + stats.tload = performance.now(); + stats.loading.first = stats.loading.first || stats.tload; + stats.loading.end = stats.tload; + if (status < 200 || status >= 300) { + callbacks.onError && callbacks.onError(response, context, null, stats); + return; + } + const responseText = r.responseText || r.response || ""; + const data = wantsArrayBuffer ? binaryTextToArrayBuffer(responseText) : responseText; + stats.loaded = data && data.byteLength ? data.byteLength : data && data.length ? data.length : stats.loaded || 0; + stats.total = stats.total || stats.loaded; + stats.bwEstimate = stats.loading.end > stats.loading.first ? Math.round(stats.total * 8e3 / (stats.loading.end - stats.loading.first)) : 0; + callbacks.onSuccess && callbacks.onSuccess({data, url: response.url}, stats, context, response); + }, + onerror: () => { + callbacks.onError && callbacks.onError({ + code: 0, + text: "network error", + url: requestUrl + }, context, null, stats); + }, + ontimeout: () => { + stats.tload = performance.now(); + stats.loading.end = stats.tload; + callbacks.onTimeout && callbacks.onTimeout(stats, context, null); + } }); - if ((result == null ? void 0 : result.dmmQualityMap) && Object.keys(result.dmmQualityMap).length > 0) { - return result.dmmQualityMap; - } - if (result == null ? void 0 : result.url) { - const qualityMap = {}; - qualityMap["720p"] = result.url; - return qualityMap; + } + } + + return GMHlsLoader; + }; + const isM3U8Url = (url) => /\.m3u8(?:[?#].*)?$/i.test(String(url || "")); + const attachHlsToVideo = (videoEl, src) => { + return new Promise((resolve, reject) => { + if (!videoEl || !src) { + reject(new Error("video element or source missing")); + return; + } + const HlsClass = getHlsClass(); + if (!HlsClass) { + videoEl.src = src; + videoEl.load && videoEl.load(); + resolve(false); + return; + } + const hls = new HlsClass({ + enableWorker: false, + lowLatencyMode: true, + loader: createHlsLoader(), + autoStartLoad: true, + startPosition: 0, + capLevelToPlayerSize: true, + testBandwidth: false, + preferManagedMediaSource: false, + maxBufferLength: 6, + maxMaxBufferLength: 12, + backBufferLength: 30, + maxBufferHole: 0.5, + nudgeOffset: 0.1, + manifestLoadingMaxRetry: 2, + levelLoadingMaxRetry: 2, + fragLoadingMaxRetry: 2, + manifestLoadingTimeOut: 12e3, + levelLoadingTimeOut: 12e3, + fragLoadingTimeOut: 12e3, + abrEwmaFastLive: 3, + abrEwmaSlowLive: 9 + }); + hls.on(HlsClass.Events.MANIFEST_PARSED, () => { + hls.startLoad(0); + videoEl.play().catch(() => { + }); + resolve(true); + }); + hls.on(HlsClass.Events.ERROR, (_, data) => { + if (!data || !data.fatal) return; + clog.error("HLS: 播放失败", data); + if (data.type === HlsClass.ErrorTypes.MEDIA_ERROR) { + if (videoEl.readyState >= 2) { + try { + hls.recoverMediaError && hls.recoverMediaError(); + } catch (e) { + } + return; + } } + reject(new Error(data.details || "HLS fatal error")); + }); + hls.loadSource(src); + hls.attachMedia(videoEl); + videoEl._hls = hls; + }); + }; + const destroyHls = (videoEl) => { + if (videoEl && videoEl._hls) { + try { + videoEl._hls.destroy(); + } catch (e) { + } + videoEl._hls = null; + } + }; + const JAVXY_TOKEN = [118, 119, 112, 71, 97, 110, 28, 84, 124, 65, 76, 102, 65, 16, 77, 109, 64, 82, 85, 83, 67, 92, 125, 108, 83, 65, 124, 107, 84, 104, 71, 84, 17, 124, 118, 125, 104, 8, 125, 96, 112, 103, 29, 18, 82, 83, 87, 84].map((v) => String.fromCharCode(v ^ 37)).join(""); + const JAVXY_ENDPOINTS = [ + {host: String.fromCharCode(106, 97, 118, 120, 121, 46, 99, 99, 46, 99, 100), label: "Javxy"}, + { + host: String.fromCharCode(119, 111, 114, 107, 101, 114, 46, 106, 97, 118, 120, 121, 46, 99, 99, 46, 99, 100), + label: "Javxy Worker" + } + ]; + const JAVXY_SOURCE_LABELS = { + "Tokyo-Hot": "Javxy | Tokyo-Hot", + FC2: "Javxy | FC2", + Direct: "Javxy | Direct", + DMM: "Javxy | dmm", + MGStage: "Javxy | MGStage", + DUGA: "Javxy | DUGA", + MYWIFE: "Javxy | MyWife", + JavTrailers: "Javxy | JavTrailers", + JavDB: "Javxy | Javdb", + AVWikiDB: "Javxy | AVWikiDB", + JAVDatabase: "Javxy | JAVDatabase", + HEYZO: "Javxy | Heyzo", + HeyDouga: "Javxy | HeyDouga", + PACO: "Javxy | Paco", + "10MU": "Javxy | 10mu", + Caribbean: "Javxy | 加勒比", + "1Pondo": "Javxy | 一本道" + }; + const JAVXY_QUALITY_OPTIONS = [ + {quality: "4k", text: "4K"}, + {quality: "hhb", text: "1080p"}, + {quality: "1080p", text: "1080p"}, + {quality: "hmb", text: "720p"}, + {quality: "720p", text: "720p"}, + {quality: "mhb", text: "576p"}, + {quality: "540p", text: "540p"}, + {quality: "mmb", text: "432p"}, + {quality: "480p", text: "480p"}, + {quality: "396p", text: "396p"}, + {quality: "360p", text: "360p"}, + {quality: "240p", text: "240p"} + ]; + const selectHighestQuality = (qualityMap) => { + const rank = new Map(JAVXY_QUALITY_OPTIONS.map((item, index) => [item.quality, index])); + return Object.keys(qualityMap || {}).filter((key) => qualityMap[key]).sort((a, b) => (rank.get(a) ?? -1) - (rank.get(b) ?? -1))[0] || null; + }; + const sortQualityKeys = (qualityMap) => { + const rank = new Map(JAVXY_QUALITY_OPTIONS.map((item, index) => [item.quality, index])); + return Object.keys(qualityMap || {}).filter((key) => qualityMap[key]).sort((a, b) => (rank.get(a) ?? -1) - (rank.get(b) ?? -1)); + }; + const normalizeJavxySource = (value) => { + const raw = String(value || "").trim().toLowerCase(); + if (raw.includes("fc2")) return "FC2"; + if (raw.includes("mgstage")) return "MGStage"; + if (raw.includes("heydouga")) return "Direct"; + if (raw.includes("mywife")) return "MYWIFE"; + if (raw.includes("duga")) return "DUGA"; + if (raw.includes("javtrailers")) return "JavTrailers"; + if (raw.includes("javdb")) return "JavDB"; + if (raw.includes("avwikidb")) return "AVWikiDB"; + if (raw.includes("javdatabase")) return "JAVDatabase"; + if (raw.includes("dmm")) return "DMM"; + if (raw === "direct" || raw.includes("heyzo") || raw.includes("heydouga") || raw.includes("paco") || raw.includes("10musume") || raw.includes("10mu") || raw.includes("1pondo") || raw.includes("caribbean") || raw.includes("tokyo-hot") || raw.includes("tokyohot")) return "Direct"; + return String(value || "").trim(); + }; + const javxyQualityMapToDmmFormat = (javxyQualityMap) => { + const result = {}; + const qualityKeys = qualityOptions.map((o) => o.quality); + for (const [key, url] of Object.entries(javxyQualityMap || {})) { + if (!url) continue; + const lowerKey = key.toLowerCase(); + if (qualityKeys.includes(key)) { + result[key] = url; + } else if (qualityKeys.includes(lowerKey)) { + result[lowerKey] = url; + } + } + return result; + }; + const fromJavxyCcCd = async (id, rawCode = "", options = {}) => { + const query = String(id || rawCode || "").trim(); + if (!query) { + clog.debug("Javxy 跳过:查询词为空"); + return null; + } + for (const endpoint of JAVXY_ENDPOINTS) { + const params = new URLSearchParams({client: "laosiji-new"}); + if (Array.isArray(options.skip) && options.skip.length) params.set("skip", options.skip.join(",")); + if (Array.isArray(options.prefer) && options.prefer.length) params.set("prefer", options.prefer.join(",")); + if (Array.isArray(options.source) && options.source.length) params.set("source", options.source.join(",")); + if (options.playbackFallback) params.set("purpose", "playback-fallback"); + const apiUrl2 = `https://${endpoint.host}/trailers/${encodeURIComponent(query)}?${params}`; + clog.debug("Javxy 请求 API", {query, apiUrl: apiUrl2, endpoint: endpoint.label}); + let r; + try { + r = await gmHttp.get(apiUrl2, null, { + "Accept": "application/json,text/plain,*/*", + "X-Javxy-Token": JAVXY_TOKEN + }, {timeout: 8e3}); + } catch (e) { + clog.debug("Javxy API 网络失败,尝试下一个节点", {endpoint: endpoint.label, error: e.message}); + continue; + } + if (!r) { + clog.debug("Javxy API 无响应,尝试下一个节点", {endpoint: endpoint.label}); + continue; + } + const trailerUrl = String((r == null ? void 0 : r.trailer) || "").trim(); + if (!trailerUrl) { + clog.debug("Javxy 无 trailer 字段", {endpoint: endpoint.label, keys: Object.keys(r || {})}); return null; - }; + } + const qualityMap = (r == null ? void 0 : r.qualities) && typeof r.qualities === "object" ? r.qualities : {}; + const quality = (r == null ? void 0 : r.quality) && qualityMap[r.quality] ? r.quality : selectHighestQuality(qualityMap); + const sourceBase = JAVXY_SOURCE_LABELS[r == null ? void 0 : r.source] || `Javxy | ${(r == null ? void 0 : r.source) || "dmm"}`; + const directUrl = qualityMap[quality] || trailerUrl; + clog.debug("Javxy 返回结果", { + endpoint: endpoint.label, + source: r == null ? void 0 : r.source, + quality, + qualities: Object.keys(qualityMap), + url: directUrl + }); + const dmmQualityMap = javxyQualityMapToDmmFormat(qualityMap); + return { + url: directUrl, + source: sourceBase, + type: String((r == null ? void 0 : r.type) || "").trim() || "video", + qualities: qualityMap, + quality, + directUrl, + code: id, + rawCode, + javxySource: String((r == null ? void 0 : r.source) || "").trim(), + requiresJP: Boolean(r == null ? void 0 : r.requiresJP), + urls: Array.isArray(r == null ? void 0 : r.urls) && r.urls.length ? r.urls : sortQualityKeys(qualityMap).map((key) => qualityMap[key]), + dmmQualityMap + }; + } + return null; + }; + const fallbackJavxyResult = async (code, rawCode = "", failedSources = [], options = {}) => { + const skip = [...new Set((failedSources || []).map((source2) => normalizeJavxySource(source2)).filter(Boolean))]; + const source = [...new Set((options.source || []).map((s) => String(s || "").trim()).filter(Boolean))]; + if (!skip.length && !source.length) return null; + clog.debug("Javxy 播放失败回落查询", {code, skip, source}); + return fromJavxyCcCd(code, rawCode, {skip, source, playbackFallback: true}); + }; + const getJavxyVideoUrls = async (code, failedSources = []) => { + const result = await fallbackJavxyResult(code, code, failedSources, { + source: ["JavTrailers", "JavDB"] + }); + if ((result == null ? void 0 : result.dmmQualityMap) && Object.keys(result.dmmQualityMap).length > 0) { + return result.dmmQualityMap; + } + if (result == null ? void 0 : result.url) { + const qualityMap = {}; + qualityMap["720p"] = result.url; + return qualityMap; + } + return null; + }; - class JavxyPreviewVideoPlugin extends BasePlugin { - getName() { - return "JavxyPreviewVideoPlugin"; - } + class JavxyPreviewVideoPlugin extends BasePlugin { + getName() { + return "JavxyPreviewVideoPlugin"; + } - _attachVideoSrc(videoEl, src) { - if (!videoEl || !src) return; - destroyHls(videoEl); - if (isM3U8Url(src)) { - const HlsClass = getHlsClass(); - if (HlsClass) { - attachHlsToVideo(videoEl, src).catch((err) => { - clog.error("视频预览 HLS 加载失败:", err); - videoEl.src = src; - videoEl.load && videoEl.load(); - }); - } else { - loadHlsLibrary().then((HlsClass2) => { - if (videoEl && videoEl.isConnected && HlsClass2) { - attachHlsToVideo(videoEl, src).catch((err) => { - clog.error("视频预览 HLS 延迟加载失败:", err); - videoEl.src = src; - videoEl.load && videoEl.load(); - }); - } else if (videoEl && videoEl.isConnected) { - videoEl.src = src; - videoEl.load && videoEl.load(); - } - }); - } - } else { + _attachVideoSrc(videoEl, src) { + if (!videoEl || !src) return; + destroyHls(videoEl); + if (isM3U8Url(src)) { + const HlsClass = getHlsClass(); + if (HlsClass) { + attachHlsToVideo(videoEl, src).catch((err) => { + clog.error("视频预览 HLS 加载失败:", err); + videoEl.src = src; + videoEl.load && videoEl.load(); + }); + } else { + loadHlsLibrary().then((HlsClass2) => { + if (videoEl && videoEl.isConnected && HlsClass2) { + attachHlsToVideo(videoEl, src).catch((err) => { + clog.error("视频预览 HLS 延迟加载失败:", err); videoEl.src = src; videoEl.load && videoEl.load(); + }); + } else if (videoEl && videoEl.isConnected) { + videoEl.src = src; + videoEl.load && videoEl.load(); } + }); } + } else { + videoEl.src = src; + videoEl.load && videoEl.load(); + } + } - _selectDefaultQuality(qualityList, intendedDefault) { - if (!qualityList || qualityList.length === 0) return null; - const availableSet = new Set(qualityList); - if (availableSet.has(intendedDefault)) return intendedDefault; - const priorityOrder = qualityOptions.map((o) => o.quality).reverse(); - for (const q of priorityOrder) if (availableSet.has(q)) return q; - return qualityList[0]; - } + _selectDefaultQuality(qualityList, intendedDefault) { + if (!qualityList || qualityList.length === 0) return null; + const availableSet = new Set(qualityList); + if (availableSet.has(intendedDefault)) return intendedDefault; + const priorityOrder = qualityOptions.map((o) => o.quality).reverse(); + for (const q of priorityOrder) if (availableSet.has(q)) return q; + return qualityList[0]; + } - async initCss() { - return ` + async initCss() { + return ` .jhs-javxy-video-modal { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.95); z-index: 12345700; @@ -10259,54 +10256,54 @@ ${err.stack}` : ""); color: #fff; font-size: 16px; text-align: center; } `; - } + } - async handle() { - if (!window.isDetailPage) return; - const carNum2 = this.getPageInfo().carNum; - if (!carNum2) return; - this._addJavxyButton(carNum2); - } + async handle() { + if (!window.isDetailPage) return; + const carNum2 = this.getPageInfo().carNum; + if (!carNum2) return; + this._addJavxyButton(carNum2); + } - _addJavxyButton(carNum2) { - let $target = null; - if (isJavDb$1) { - $target = $('a[title="複製番號"]'); - } else if (isJavBus$1) { - const headerSpans = document.querySelectorAll("span.header"); - for (const span of headerSpans) { - if (span.textContent.trim() === "識別碼:") { - const nextSpan = span.nextElementSibling; - if (nextSpan && nextSpan.tagName === "SPAN") { - const copyBtn = nextSpan.nextElementSibling; - if (copyBtn && copyBtn.tagName === "BUTTON" && copyBtn.textContent.trim() === "复制") { - $target = $(copyBtn); - } else { - $target = $(nextSpan); - } - } - break; - } - } + _addJavxyButton(carNum2) { + let $target = null; + if (isJavDb$1) { + $target = $('a[title="複製番號"]'); + } else if (isJavBus$1) { + const headerSpans = document.querySelectorAll("span.header"); + for (const span of headerSpans) { + if (span.textContent.trim() === "識別碼:") { + const nextSpan = span.nextElementSibling; + if (nextSpan && nextSpan.tagName === "SPAN") { + const copyBtn = nextSpan.nextElementSibling; + if (copyBtn && copyBtn.tagName === "BUTTON" && copyBtn.textContent.trim() === "复制") { + $target = $(copyBtn); + } else { + $target = $(nextSpan); + } } - if (!$target || !$target.length) return; - const $btn = $(`视频预览`); - $btn.on("click", async (e) => { - e.preventDefault(); - e.stopPropagation(); - $btn.css("pointer-events", "none").find("span").text("..."); - try { - await this._openJavxyPlayer(carNum2); - } finally { - $btn.css("pointer-events", "").find("span").text("视频预览"); - } - }); - $target.after($btn); + break; + } } + } + if (!$target || !$target.length) return; + const $btn = $(`视频预览`); + $btn.on("click", async (e) => { + e.preventDefault(); + e.stopPropagation(); + $btn.css("pointer-events", "none").find("span").text("..."); + try { + await this._openJavxyPlayer(carNum2); + } finally { + $btn.css("pointer-events", "").find("span").text("视频预览"); + } + }); + $target.after($btn); + } - async _openJavxyPlayer(carNum2) { - if ($("#jhs-javxy-video-modal").length === 0) { - $("body").append(` + async _openJavxyPlayer(carNum2) { + if ($("#jhs-javxy-video-modal").length === 0) { + $("body").append(`
@@ -10316,88 +10313,88 @@ ${err.stack}` : "");
`); - const $modal2 = $("#jhs-javxy-video-modal"); - $modal2.on("click", (e) => { - if (e.target.id === "jhs-javxy-video-modal") { - $modal2.removeClass("is-open"); - const videoEl2 = document.getElementById("jhs-javxy-video"); - if (videoEl2) { - destroyHls(videoEl2); - videoEl2.pause(); - } - } - }); - $(document).on("keydown", (e) => { - if (e.key === "Escape" && $modal2.hasClass("is-open")) { - $modal2.removeClass("is-open"); - const videoEl2 = document.getElementById("jhs-javxy-video"); - if (videoEl2) { - destroyHls(videoEl2); - videoEl2.pause(); - } - } - }); + const $modal2 = $("#jhs-javxy-video-modal"); + $modal2.on("click", (e) => { + if (e.target.id === "jhs-javxy-video-modal") { + $modal2.removeClass("is-open"); + const videoEl2 = document.getElementById("jhs-javxy-video"); + if (videoEl2) { + destroyHls(videoEl2); + videoEl2.pause(); } - const $modal = $("#jhs-javxy-video-modal"); - const $wrapper = $modal.find(".jhs-javxy-video-wrapper"); - const $qualityBar = $modal.find(".jhs-javxy-quality-bar"); - $wrapper.html('
视频预览加载中...
'); - $qualityBar.empty(); - $modal.addClass("is-open"); - let videoMap = null; - try { - videoMap = await getJavxyVideoUrls(carNum2); - } catch (err) { - $wrapper.html(`
视频预览请求失败: ${err.message}
`); - return; + } + }); + $(document).on("keydown", (e) => { + if (e.key === "Escape" && $modal2.hasClass("is-open")) { + $modal2.removeClass("is-open"); + const videoEl2 = document.getElementById("jhs-javxy-video"); + if (videoEl2) { + destroyHls(videoEl2); + videoEl2.pause(); } - if (!videoMap || Object.keys(videoMap).length === 0) { - $wrapper.html('
未找到可用的视频源
'); - return; - } - const qualityList = Object.keys(videoMap); - const defaultQuality = this._selectDefaultQuality(qualityList, "720p"); - const defaultUrl = videoMap[defaultQuality]; - $wrapper.html(`