@@ -6775,10 +6969,10 @@ ${value}\r
}
static openSearchModal({details = {}, getTargetCid} = {}) {
- var _a2;
+ var _a;
const defaultKw = this.buildDefaultKeyword(details);
this.currentDetails = details;
- (_a2 = document.getElementById(this.MODAL_ID)) == null ? void 0 : _a2.remove();
+ (_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";
@@ -6788,6 +6982,27 @@ ${value}\r
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" : "";
+ 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 = "";
@@ -6802,26 +7017,34 @@ ${value}\r
contentWrap.innerHTML = '
正在搜索字幕来源,请稍候...
';
previewBox.value = "";
statusNode.textContent = "暂无预览";
- 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;
+ 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 = '
搜索失败,请稍后重试
';
}
- this.renderTable({
- container: contentWrap,
- dataList: chineseResults,
- previewBox,
- statusNode,
- overlay,
- details,
- getTargetCid,
- kw
- });
};
overlay.querySelector("#sub-search-btn").addEventListener("click", () => performSearch(input.value.trim()));
input.addEventListener("keypress", (e) => {
@@ -6831,23 +7054,12 @@ ${value}\r
}
static requestText(url) {
- return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
- method: "GET",
- url,
- onload: (res) => {
- if (res.status >= 200 && res.status < 300) resolve(res.responseText || "");
- else reject(new Error(`HTTP ${res.status}`));
- },
- onerror: () => reject(new Error("网络请求失败"))
- });
- });
+ return window.gmHttp.get(url);
}
static async searchXunlei(kw) {
- const text = await this.requestText(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name=${encodeURIComponent(kw)}`);
- const root = JSON.parse(text);
- if (root.code !== 0 || !Array.isArray(root.data)) return [];
+ 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);
@@ -6902,9 +7114,9 @@ ${value}\r
static parseSubtitleCatSearch(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
return [...doc.querySelectorAll("table.sub-table tbody tr")].flatMap((row) => {
- var _a2;
+ var _a;
const link = row.querySelector('a[href*="subs/"]');
- const name2 = (_a2 = link == null ? void 0 : link.textContent) == null ? void 0 : _a2.trim();
+ const name2 = (_a = link == null ? void 0 : link.textContent) == null ? void 0 : _a.trim();
if (!link || !name2) return [];
return [{
name: name2,
@@ -6916,8 +7128,8 @@ ${value}\r
static parseSubtitleCatLanguages(html, entry) {
const doc = new DOMParser().parseFromString(html, "text/html");
return [...doc.querySelectorAll(".sub-single")].flatMap((node) => {
- var _a2;
- const languageCode = ((_a2 = node.querySelector("img.flag")) == null ? void 0 : _a2.getAttribute("alt")) || "";
+ 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 [];
@@ -6994,8 +7206,8 @@ ${value}\r
}
static renderRow(item, index, highlightRegex) {
- var _a2;
- const lang = ((_a2 = item.languages) == null ? void 0 : _a2[0]) || "未知";
+ 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";
@@ -7016,7 +7228,6 @@ ${value}\r
}
static async handleAction({btn, item, previewBox, statusNode, overlay, details, getTargetCid}) {
- var _a2, _b, _c;
const action = btn.dataset.action;
const oldText = btn.textContent;
const url = item.url;
@@ -7035,21 +7246,24 @@ ${value}\r
} else if (action === "download") {
this.downloadBuffer(buffer, filename);
} else if (action === "upload") {
- const cid = (getTargetCid == null ? void 0 : getTargetCid()) || this.getTargetCid();
+ const jdbCid = (getTargetCid == null ? void 0 : getTargetCid()) || this.getTargetCid();
+ let cids = jdbCid ? [jdbCid] : [...this.current115Cids];
const req115 = this.getReq115();
- if (!cid || !req115) throw new Error("未检测到 115 目标目录,请先等待匹配完成");
+ if (!cids.length || !req115) throw new Error("未在 115 找到匹配目录,请确认作品已下载到 115");
btn.textContent = "直传中...";
- await this.uploadSubtitle({req115, cid, filename, buffer});
- (_b = (_a2 = this.getGrant()) == null ? void 0 : _a2.notify) == null ? void 0 : _b.call(_a2, {
- icon: "success",
- msg: "字幕已上传到 115"
- });
- (_c = document.getElementById(this.BTN_ID)) == null ? void 0 : _c.classList.add("is-success");
- document.getElementById(this.BTN_ID).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(cid),
+ cid: String(cids[0]),
subtitle: {n: filename, s: buffer.byteLength}
}
}));
@@ -7091,7 +7305,7 @@ ${value}\r
static fetchBinary(url) {
return new Promise((resolve, reject) => {
- GM_xmlhttpRequest({
+ _GM_xmlhttpRequest({
method: "GET",
url,
responseType: "arraybuffer",
@@ -7133,10 +7347,492 @@ ${value}\r
}
throw new Error((uploadRes == null ? void 0 : uploadRes.error_msg) || (uploadRes == null ? void 0 : uploadRes.error) || "115 服务器拒绝接收回调");
}
- }, __publicField(_a, "BTN_ID", "x-subtitle-search-btn"), __publicField(_a, "MODAL_ID", "x-subtitle-modal"), __publicField(_a, "previewCache", /* @__PURE__ */ new Map()), __publicField(_a, "SUBTITLECAT_ORIGIN", "https://www.subtitlecat.com"), __publicField(_a, "SUBTITLECAT_CHINESE_LANGUAGES", /* @__PURE__ */ new Map([
+ }
+
+ __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", "繁中"]
- ])), _a);
+ ]));
+ 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;
+ }
+ }
+
+ __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]");
+
+ class Drive115 extends Req {
+ static files(cid = "0", params = {}) {
+ return this.request({
+ url: "https://webapi.115.com/files",
+ params: {cid, ...params}
+ });
+ }
+
+ static filesSearch(search_value, params = {}) {
+ return this.request({
+ url: "https://webapi.115.com/files/search",
+ params: {search_value, ...params}
+ });
+ }
+
+ static lixianTaskLists(page = 1) {
+ return this.request({
+ url: "https://115.com/web/lixian/",
+ params: {ct: "lixian", ac: "task_lists", page}
+ });
+ }
+
+ static labelList() {
+ return this.request({
+ url: "https://webapi.115.com/label/list",
+ params: {keyword: "", limit: 11150}
+ });
+ }
+
+ /**
+ * @connect 115vod.com
+ */
+ static filesVideo(pickcode) {
+ return this.request({
+ url: "https://115vod.com/webapi/files/video",
+ params: {pickcode, local: 1}
+ });
+ }
+
+ static post(details) {
+ return this.request({method: "POST", ...details});
+ }
+
+ static filesAdd(cname, pid) {
+ return this.post({
+ url: "https://webapi.115.com/files/add",
+ data: {cname, pid}
+ });
+ }
+
+ 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}
+ });
+ }
+
+ /**
+ * 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}
+ });
+ }
+
+ /**
+ * 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,
+ accessid: OSSAccessKeyId,
+ callback,
+ signature,
+ file
+ }) {
+ return this.post({
+ url,
+ data: {
+ name: name2,
+ key,
+ policy,
+ OSSAccessKeyId,
+ success_action_status: "200",
+ callback,
+ signature,
+ file
+ }
+ });
+ }
+
+ static filesEdit(fid, fid_cover) {
+ return this.post({
+ url: "https://webapi.115.com/files/edit",
+ data: {fid, fid_cover}
+ });
+ }
+ }
+
+ __publicField(Drive115, "DEFAULT_GET_RESPONSE_TYPE", "json");
+
+ 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;
+ }
+
+ 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";
@@ -7228,6 +7924,7 @@ ${value}\r
$("#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({
@@ -7312,7 +8009,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);
+ imgUrl && await screenShotPlugin.addImg("缩略图", imgUrl, carNum2);
}));
}
handleMagnets(movieId) {
@@ -8916,6 +9613,428 @@ ${err.stack}` : "");
return results;
}
}
+
+ const Thumbnail = {
+ sources: ["javfree", "projectjav", "javstore"],
+ cacheKey(code) {
+ return `thumb_cache_v3_${code}`;
+ },
+ lookupCode(code) {
+ const text = String(code || "").trim();
+ const fc2 = text.match(/^(?:FC2[-_\s]?(?:PPV[-_\s]?)?)?(\d{6,9})$/i);
+ return fc2 ? fc2[1] : text;
+ },
+ sourceOrder() {
+ const savedOrder = ["javfree", "projectjav", "javstore"];
+ const seen = /* @__PURE__ */ new Set();
+ return [...savedOrder, ...this.sources].filter((src) => {
+ if (seen.has(src) || typeof this[src] !== "function") return false;
+ seen.add(src);
+ return true;
+ });
+ },
+ async fetchFromSource(source, code) {
+ try {
+ return await this[source](this.lookupCode(code));
+ } catch (e) {
+ clog.debug(`Thumbnail[${source}] 异常:`, e.message);
+ return null;
+ }
+ },
+ normalizeForCompare(text) {
+ return String(text || "").toLowerCase().replace(/[^a-z0-9]/g, "");
+ },
+ isCodeMatched(text, code) {
+ const normalizedText = this.normalizeForCompare(text);
+ const normalizedCode = this.normalizeForCompare(code);
+ return !!normalizedCode && normalizedText.includes(normalizedCode);
+ },
+ isDetailMatched(doc, url, code) {
+ var _a, _b;
+ const title = ((_a = doc == null ? void 0 : doc.querySelector("title")) == null ? void 0 : _a.textContent) || "";
+ const headings = [...(doc == null ? void 0 : doc.querySelectorAll("h1,h2,h3,.entry-title,.movie-title,.post-title")) || []].map((el) => el.textContent || "").join(" ");
+ const bodyText = (((_b = doc == null ? void 0 : doc.body) == null ? void 0 : _b.textContent) || "").slice(0, 5e3);
+ return this.isCodeMatched([url, title, headings, bodyText].join(" "), code);
+ },
+ normalizePreviewUrl(url, baseUrl = "") {
+ if (!url) return "";
+ const absolute = /^https?:\/\//i.test(url) ? url : baseUrl ? new URL(url, baseUrl).href : url;
+ return absolute.replace(/^http:/, "https:");
+ },
+ isJavfreePreviewImage(url, code) {
+ const cleanUrl = String(url || "").split("?")[0];
+ const lookupCode = this.lookupCode(code);
+ const isFc2Numeric = /^\d{6,9}$/.test(lookupCode);
+ const fc2ShotPattern = isFc2Numeric ? new RegExp(`${lookupCode}_\\d+\\.(?:jpe?g|png|webp)$`, "i") : null;
+ return this.isCodeMatched(cleanUrl, code) && (/-(?:1080p|demosaic)\.(?:jpe?g|png|webp)$/i.test(cleanUrl) || isFc2Numeric && fc2ShotPattern.test(cleanUrl));
+ },
+ selectJavfreePreviewUrl(doc, detailUrl, code) {
+ const urls = [...doc.querySelectorAll("p > img[src]")].map((img) => this.normalizePreviewUrl(img.getAttribute("src") || img.src || "", detailUrl)).filter((url) => this.isJavfreePreviewImage(url, code));
+ return urls.find((url) => /-1080p\./i.test(url)) || urls.find((url) => /-demosaic\./i.test(url)) || urls.find((url) => /_1\.(?:jpe?g|png|webp)$/i.test(url)) || "";
+ },
+ async javfree(code) {
+ var _a;
+ code = this.lookupCode(code);
+ const cacheKey = this.cacheKey(code);
+ const cached = sessionStorage.getItem(cacheKey);
+ if (cached) return cached;
+ try {
+ const html = await gmHttp.get(`https://javfree.me/search/${code}`);
+ const doc = new DOMParser().parseFromString(html, "text/html");
+ const link = (_a = [...doc.querySelectorAll(".entry-title>a")].find((a) => this.isCodeMatched([a.href, a.textContent].join(" "), code))) == null ? void 0 : _a.href;
+ if (!link) return null;
+ const dHtml = await gmHttp.get(link);
+ const dDoc = new DOMParser().parseFromString(dHtml, "text/html");
+ if (!this.isDetailMatched(dDoc, link, code)) return null;
+ const url = this.selectJavfreePreviewUrl(dDoc, link, code);
+ if (url) {
+ sessionStorage.setItem(cacheKey, url);
+ return url;
+ }
+ return null;
+ } catch {
+ return null;
+ }
+ },
+ async javstore(code) {
+ code = this.lookupCode(code);
+ try {
+ const normalizedCode = code.replace(/^fc2-?/i, "").replace(/-/g, "").toLowerCase();
+ const searchUrl = `https://javstore.net/search?q=${encodeURIComponent(code)}`;
+ const searchHtml = await gmHttp.get(searchUrl);
+ const searchDoc = new DOMParser().parseFromString(searchHtml, "text/html");
+ const candidateLinks = searchDoc.querySelectorAll('a[href*="/"]');
+ const detailUrls = [];
+ for (const link of candidateLinks) {
+ const href = link.getAttribute("href");
+ if (!href) continue;
+ if (href.startsWith("http") && !href.includes("javstore.net")) continue;
+ const urlObj = new URL(href, searchUrl);
+ if (!/javstore\.net$/i.test(urlObj.hostname)) continue;
+ if (/^\/search(?:[/?#]|$)/i.test(urlObj.pathname)) continue;
+ const fullUrl = urlObj.href;
+ const pathLastPart = decodeURIComponent(urlObj.pathname.split("/").pop() || "");
+ const normalizedPath = pathLastPart.toLowerCase().replace(/-/g, "");
+ const looksLikeDetail = /\.html$/i.test(urlObj.pathname) || /^\/\d+[-/]/.test(urlObj.pathname);
+ if (looksLikeDetail && normalizedPath.includes(normalizedCode) && !detailUrls.includes(fullUrl)) {
+ detailUrls.push(fullUrl);
+ }
+ }
+ if (detailUrls.length === 0) return null;
+ for (const detailUrl of detailUrls) {
+ const imgUrl = await this._extractImgFromDetail(detailUrl, code);
+ if (imgUrl) return imgUrl;
+ }
+ return null;
+ } catch (e) {
+ clog.debug("javstore 获取失败", e);
+ return null;
+ }
+ },
+ async _extractImgFromDetail(detailUrl, code) {
+ try {
+ const detailHtml = await gmHttp.get(detailUrl);
+ const detailDoc = new DOMParser().parseFromString(detailHtml, "text/html");
+ if (!this.isDetailMatched(detailDoc, detailUrl, code)) return null;
+ for (const link of detailDoc.querySelectorAll("a")) {
+ if (link.textContent.includes("CLICK HERE")) {
+ const imgUrl = link.href || link.getAttribute("href") || "";
+ if (imgUrl) return this.normalizePreviewUrl(imgUrl, detailUrl);
+ }
+ }
+ const img = detailDoc.querySelector('img[src*="_s.jpg"]');
+ if (img) {
+ let src = img.getAttribute("src") || "";
+ if (!src.startsWith("http")) src = new URL(src, detailUrl).href;
+ return this.normalizePreviewUrl(src.replace(/_s\.jpg$/, "_l.jpg"), detailUrl);
+ }
+ return null;
+ } catch (e) {
+ clog.debug("javstore: 详情页请求失败", detailUrl, e.message);
+ return null;
+ }
+ },
+ async projectjav(code) {
+ var _a;
+ code = this.lookupCode(code);
+ try {
+ const request = (url) => gmHttp.get(url, {}, {
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
+ "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
+ "Accept-Language": "zh-CN,zh;q=0.9,en;q=0.8"
+ });
+ const searchUrl = `https://projectjav.com/?searchTerm=${encodeURIComponent(code)}`;
+ const searchHtml = await request(searchUrl);
+ const searchDoc = new DOMParser().parseFromString(searchHtml, "text/html");
+ let detailUrl = "";
+ const allMovieLinks = [...searchDoc.querySelectorAll('a[href*="/movie/"]')];
+ const firstLink = ((_a = allMovieLinks[0]) == null ? void 0 : _a.getAttribute("href")) || "";
+ if (firstLink) {
+ detailUrl = firstLink.startsWith("http") ? firstLink : `https://projectjav.com${firstLink}`;
+ }
+ if (!detailUrl) return null;
+ const detailHtml = await request(detailUrl);
+ const detailDoc = new DOMParser().parseFromString(detailHtml, "text/html");
+ const screenshotLink = [...detailDoc.querySelectorAll('.col-md-12.thumbnail a[data-featherlight="image"], .thumbnail a[data-featherlight="image"]')].find((a) => {
+ var _a2;
+ return this.isCodeMatched([a.outerHTML, (_a2 = a.closest(".thumbnail")) == null ? void 0 : _a2.outerHTML, detailUrl].join(" "), code);
+ });
+ if (screenshotLink) {
+ const thumbImg = screenshotLink.querySelector("img");
+ const href = screenshotLink.getAttribute("href") || "";
+ if (href) return this.normalizePreviewUrl(href, detailUrl);
+ if (thumbImg) {
+ const src = (thumbImg.getAttribute("src") || "").replace(/\?.*$/, "");
+ if (src) return this.normalizePreviewUrl(src, detailUrl);
+ }
+ }
+ return null;
+ } catch (e) {
+ clog.debug("[projectjav] 异常:", e.message);
+ return null;
+ }
+ },
+ async get(code) {
+ const cacheKey = this.cacheKey(code);
+ const cached = sessionStorage.getItem(cacheKey);
+ if (cached) return {url: cached, source: null};
+ let url = null, source = null;
+ for (const src of this.sourceOrder()) {
+ url = await this.fetchFromSource(src, code);
+ if (url) {
+ source = src;
+ break;
+ }
+ }
+ if (url) sessionStorage.setItem(cacheKey, url);
+ return {url, source};
+ }
+ };
+ let previewOverlay = null;
+
+ function showPreviewOverlay$1(imgUrl, code, source = null) {
+ if (previewOverlay) {
+ previewOverlay.close();
+ }
+ let currentBlobUrl = null;
+ const originalHtmlOverflow = document.documentElement.style.overflow;
+ const originalBodyOverflow = document.body.style.overflow;
+ document.documentElement.style.overflow = "hidden";
+ document.body.style.overflow = "hidden";
+ const container = document.createElement("div");
+ container.className = "jhs-preview-overlay";
+ const img = document.createElement("img");
+ img.src = imgUrl;
+ img.className = "jhs-preview-img";
+ img.onclick = (e) => {
+ e.stopPropagation();
+ img.classList.toggle("zoomed");
+ };
+ img.onerror = () => {
+ img.src = imgUrl;
+ };
+ const toolbar = document.createElement("div");
+ toolbar.className = "jhs-preview-toolbar";
+ const createButton = (text, className, onClick) => {
+ const btn = document.createElement("button");
+ btn.className = `jhs-preview-btn ${className}`;
+ btn.textContent = text;
+ btn.onclick = onClick;
+ return btn;
+ };
+ const setActiveSource = (activeSource) => {
+ toolbar.querySelectorAll(".jhs-preview-btn").forEach((b) => b.classList.remove("active"));
+ const activeBtn = toolbar.querySelector(`.jhs-preview-btn-${activeSource}`);
+ if (activeBtn) activeBtn.classList.add("active");
+ };
+ const changeSource = async (src, srcName) => {
+ const loadingOverlay = document.createElement("div");
+ loadingOverlay.className = "jhs-loading-overlay";
+ loadingOverlay.innerHTML = '
';
+ document.body.appendChild(loadingOverlay);
+ try {
+ const newUrl = src === "javfree" ? await Thumbnail.javfree(code) : src === "projectjav" ? await Thumbnail.projectjav(code) : await Thumbnail.javstore(code);
+ if (newUrl) {
+ if (currentBlobUrl) {
+ URL.revokeObjectURL(currentBlobUrl);
+ currentBlobUrl = null;
+ }
+ img.src = newUrl;
+ setActiveSource(src);
+ } else {
+ showToast(`${srcName} 未找到预览图`, "error");
+ }
+ } catch (err) {
+ showToast(`${srcName} 加载失败: ${err.message}`, "error");
+ } finally {
+ loadingOverlay.remove();
+ }
+ };
+ let toastTimer = null;
+ const showToast = (msg, type = "info") => {
+ const existing = container.querySelector(".jhs-preview-toast");
+ if (existing) existing.remove();
+ if (toastTimer) clearTimeout(toastTimer);
+ const toast = document.createElement("div");
+ toast.className = `jhs-preview-toast jhs-preview-toast-${type}`;
+ toast.textContent = msg;
+ container.appendChild(toast);
+ requestAnimationFrame(() => toast.classList.add("show"));
+ toastTimer = setTimeout(() => {
+ toast.classList.remove("show");
+ setTimeout(() => toast.remove(), 300);
+ }, 3e3);
+ };
+ const javfreeBtn = createButton("javfree", "jhs-preview-btn-javfree javfree", (e) => {
+ e.stopPropagation();
+ changeSource("javfree", "javfree");
+ });
+ const projectjavBtn = createButton("projectjav", "jhs-preview-btn-projectjav projectjav", (e) => {
+ e.stopPropagation();
+ changeSource("projectjav", "projectjav");
+ });
+ const javstoreBtn = createButton("javstore", "jhs-preview-btn-javstore javstore", (e) => {
+ e.stopPropagation();
+ changeSource("javstore", "javstore");
+ });
+ const newWindowBtn = createButton("新窗口打开", "jhs-preview-btn-action action", (e) => {
+ e.stopPropagation();
+ window.open(img.src);
+ });
+ const downloadBtn = createButton("下载", "jhs-preview-btn-action action", (e) => {
+ e.stopPropagation();
+ const a = document.createElement("a");
+ a.href = img.src;
+ a.download = `${code}.jpg`;
+ a.click();
+ });
+ toolbar.appendChild(javfreeBtn);
+ toolbar.appendChild(projectjavBtn);
+ toolbar.appendChild(javstoreBtn);
+ toolbar.appendChild(newWindowBtn);
+ toolbar.appendChild(downloadBtn);
+ if (source === "javfree") setActiveSource("javfree");
+ else if (source === "projectjav") setActiveSource("projectjav");
+ else if (source === "javstore") setActiveSource("javstore");
+ else setActiveSource("javfree");
+ container.appendChild(img);
+ const closeOverlay = () => {
+ if (container.parentNode) {
+ container.remove();
+ toolbar.remove();
+ document.documentElement.style.overflow = originalHtmlOverflow;
+ document.body.style.overflow = originalBodyOverflow;
+ if (currentBlobUrl) {
+ URL.revokeObjectURL(currentBlobUrl);
+ currentBlobUrl = null;
+ }
+ }
+ };
+ container.onclick = closeOverlay;
+ const escHandler = (e) => {
+ if (e.key === "Escape") {
+ closeOverlay();
+ document.removeEventListener("keydown", escHandler);
+ }
+ };
+ document.addEventListener("keydown", escHandler);
+ document.body.appendChild(container);
+ document.body.appendChild(toolbar);
+ previewOverlay = {container, toolbar, close: closeOverlay};
+ }
+
+ class ScreenShotPlugin extends BasePlugin {
+ getName() {
+ return "ScreenShotPlugin";
+ }
+
+ async handle() {
+ this.loadScreenShot().then();
+ }
+
+ async loadScreenShot() {
+ if (!window.isDetailPage) return;
+ if ("yes" !== await storageManager.getSetting("enableLoadScreenShot", "yes")) return;
+ let carNum2 = this.getPageInfo().carNum;
+ isJavDb$1 && $(".preview-images .tile-item").first().before(
+ '
正在加载缩略图
'
+ );
+ isJavBus$1 && $("#sample-waterfall .sample-box:first").after(
+ '
正在加载缩略图
'
+ );
+ try {
+ const result = await Thumbnail.get(carNum2);
+ if (result.url) {
+ this.addImg("缩略图", result.url, carNum2, result.source);
+ clog.log("加载缩略图:", result.url, result.source);
+ } else {
+ this.showErrorFallback(carNum2, new Error("所有来源均未找到预览图"));
+ }
+ } catch (e) {
+ this.showErrorFallback(carNum2, e);
+ }
+ }
+
+ async getScreenshot(carNum2) {
+ const result = await Thumbnail.get(carNum2);
+ if (result.url) return result.url;
+ throw new Error("未找到预览图");
+ }
+
+ async getJavStoreScreenShot(carNum2) {
+ return await Thumbnail.javstore(carNum2);
+ }
+
+ async getJavFreeScreenShot(carNum2) {
+ return await Thumbnail.javfree(carNum2);
+ }
+
+ addImg(title, imgUrl, code, source) {
+ if (!imgUrl) return;
+ isJavDb$1 && $(".screen-container").html(
+ `

`
+ );
+ isJavBus$1 && $(".screen-container").html(
+ `
`
+ );
+ $(".screen-container").on("click", (event) => {
+ event.stopPropagation();
+ event.preventDefault();
+ showPreviewOverlay$1(imgUrl, code || title, source);
+ });
+ }
+
+ showErrorFallback(carNum2, error) {
+ var _a, _b;
+ console.error("获取缩略图失败:", ((_b = (_a = error == null ? void 0 : error.message) == null ? void 0 : _a.substring) == null ? void 0 : _b.call(_a, 0, 100)) || error);
+ let differentCss = isJavBus$1 ? "margin-top:30px" : "margin-top:50px";
+ $(".screen-container").html(
+ `
获取缩略图失败
点击重试 或
前往确认`
+ ).off("click", ".retry-link").off("click", ".check-link").on("click", ".retry-link", async (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ $(".screen-container").html(
+ `
正在重新加载...
`
+ );
+ try {
+ const result = await Thumbnail.get(carNum2);
+ if (result.url) {
+ this.addImg("缩略图", result.url, carNum2, result.source);
+ } else {
+ this.showErrorFallback(carNum2, new Error("所有来源均未找到预览图"));
+ }
+ } catch (err) {
+ this.showErrorFallback(carNum2, err);
+ }
+ }).on("click", ".check-link", (e) => {
+ e.stopPropagation();
+ e.preventDefault();
+ window.open(`https://javstore.net/search?q=${encodeURIComponent(carNum2)}`, "_blank");
+ });
+ }
+ }
+
+ window.showPreviewOverlay = showPreviewOverlay$1;
const getDownPathList = async () => {
const res = await gmHttp.get("https://webapi.115.com/offine/downpath");
return "object" == typeof res ? res.data : null;
@@ -9101,153 +10220,6 @@ ${err.stack}` : "");
// }
// }
}
- class ScreenShotPlugin extends BasePlugin {
- getName() {
- return "ScreenShotPlugin";
- }
- async handle() {
- this.loadScreenShot().then();
- }
- async loadScreenShot() {
- if (!window.isDetailPage) return;
- if ("yes" !== await storageManager.getSetting("enableLoadScreenShot", "yes")) return;
- let carNum2 = this.getPageInfo().carNum;
- isJavDb$1 && $(".preview-images .tile-item").first().before('
正在加载缩略图
');
- isJavBus$1 && $("#sample-waterfall .sample-box:first").after('
正在加载缩略图
');
- try {
- const imgUrl = await this.getScreenshot(carNum2);
- this.addImg("缩略图", imgUrl);
- clog.log("加载缩略图:", imgUrl);
- } catch (e) {
- this.showErrorFallback(carNum2, e);
- }
- }
- async getScreenshot(carNum2) {
- const cacheData = localStorage.getItem("jhs_screenShot") ? JSON.parse(localStorage.getItem("jhs_screenShot")) : {};
- if (cacheData[carNum2]) {
- clog.debug("缓存中存在缩略图:", carNum2, cacheData[carNum2]);
- return cacheData[carNum2];
- }
- let imgUrl;
- try {
- imgUrl = await Promise.any([this.getJavStoreScreenShot(carNum2)]);
- } catch (e) {
- clog.error("获取缩略图资源失败:", imgUrl, e);
- throw e;
- }
- if (!imgUrl) {
- this.showErrorFallback(carNum2, null);
- return null;
- }
- const httpsIndex = imgUrl.indexOf("https://");
- -1 !== httpsIndex && (imgUrl = imgUrl.substring(httpsIndex));
- cacheData[carNum2] = imgUrl;
- clog.log("缩略图获取成功:", imgUrl);
- localStorage.setItem("jhs_screenShot", JSON.stringify(cacheData));
- return imgUrl;
- }
- async getJavStoreScreenShot(carNum2) {
- let url = `https://javstore.net/search/${carNum2}.html`;
- clog.log("正在解析缩略图:", url);
- let html = await gmHttp.get(url);
- const $dom = utils.htmlTo$dom(html);
- let detailPageUrl = null;
- $dom.find("#content_news h3 span a").each((function() {
- if ($(this).attr("title").toLowerCase().includes(carNum2.toLowerCase())) {
- detailPageUrl = $(this).attr("href");
- return false;
- }
- }));
- if (!detailPageUrl) {
- clog.error("JavStore, 查询番号失败:", url);
- return null;
- }
- let detailPageHtml = await gmHttp.get(detailPageUrl);
- const $detailPageDom = utils.htmlTo$dom(detailPageHtml);
- let imgUrl = $detailPageDom.find("a:contains('CLICK HERE')").attr("href") || $detailPageDom.find("img[src*='_s.jpg']").attr("src");
- if (!imgUrl) {
- clog.error("JavStore, 解析预览图失败:", url);
- return null;
- }
- return imgUrl.replace(".th", "");
- }
- async getJavBestScreenShot(carNum2) {
- let url = `https://javbest.net/?s=${carNum2}`;
- clog.log("正在解析缩略图:", url);
- let html = await gmHttp.get(url);
- const $dom = utils.htmlTo$dom(html), href = $dom.find(".app_loop_thumb a").first().attr("href");
- if (!href) {
- clog.error("解析JavBest搜索页失败:", url);
- throw new Error("解析JavBest搜索页失败");
- }
- const title = $dom.find(".app_loop_thumb a").first().attr("title");
- if (!title.toLowerCase().includes(carNum2.toLowerCase())) {
- clog.error("解析JavBest搜索页失败:", title);
- throw new Error("解析JavBest搜索页失败");
- }
- const detailPageHtml = await gmHttp.get(href);
- let imgUrl = $(detailPageHtml).find('#content a img[src*="_t.jpg"]').attr("src");
- if (!imgUrl) {
- clog.error("解析JavBest缩略图失败:", url);
- throw new Error("解析JavBest缩略图失败");
- }
- imgUrl = imgUrl.replace("_t", "").replace("http:", "https:");
- return imgUrl;
- }
- async getJavFreeScreenShot(carNum2) {
- let url = `https://javfree.me/search/${carNum2}/`, html = await gmHttp.get(url);
- const $itemList = utils.htmlTo$dom(html).find("article h2.entry-title a");
- if (!$itemList || 0 === $itemList.length) {
- clog.error("解析JavFree搜索页失败:", url);
- throw new Error("解析JavFree搜索页失败");
- }
- let detailPageUrl = $($itemList[0]).attr("href"), detailPageHtml = await gmHttp.get(detailPageUrl);
- const $imgList = utils.htmlTo$dom(detailPageHtml).find("#main > article > .entry-content > p img");
- if (!$imgList || 0 === $imgList.length) {
- clog.error("解析JavFree详情页失败:", detailPageUrl);
- throw new Error("解析JavFree详情页失败");
- }
- const srcList = $imgList.filter((function() {
- const src = $(this).attr("src");
- return src && src.toLowerCase().endsWith(".jpeg");
- })).map((function() {
- return $(this).attr("src");
- })).get();
- console.log(srcList);
- return srcList.at(-1);
- }
- addImg(title, imgUrl) {
- if (imgUrl) {
- isJavDb$1 && $(".screen-container").html(`

`);
- isJavBus$1 && $(".screen-container").html(`
`);
- $(".screen-container").on("click", ((event) => {
- event.stopPropagation();
- event.preventDefault();
- showImageViewer(event.currentTarget);
- }));
- }
- }
- showErrorFallback(carNum2, error) {
- var _a2;
- console.error("获取缩略图失败:", null == (_a2 = null == error ? void 0 : error.message) ? void 0 : _a2.substring(0, 100));
- let differentCss = isJavBus$1 ? "margin-top: 30px" : "margin-top: 50px";
- $(".screen-container").html(`
获取缩略图失败
点击重试 或
前往确认`).off("click", ".retry-link").off("click", ".check-link").on("click", ".retry-link", (async (e) => {
- e.stopPropagation();
- e.preventDefault();
- $(".screen-container").html(`
正在重新加载...
`);
- try {
- const imgUrl = await this.getScreenshot(carNum2);
- this.addImg("缩略图", imgUrl);
- } catch (err) {
- this.showErrorFallback(carNum2, err);
- }
- })).on("click", ".check-link", (async (e) => {
- e.stopPropagation();
- e.preventDefault();
- window.open(`https://javstore.net/search/${carNum2}.html`, "_blank");
- }));
- }
- }
class WangPan115Plugin extends BasePlugin {
constructor() {
super(...arguments);
@@ -11299,7 +12271,7 @@ ${err.stack}` : "");
carNum2 = carNum2.replace("FC2-", "");
const imgUrl = await this.getBean("ScreenShotPlugin").getScreenshot(carNum2);
loadObj.close();
- showImageViewer(imgUrl);
+ showPreviewOverlay(imgUrl, carNum2);
} catch (error) {
console.error("图片预览出错:", error);
show.error("图片预览出错:" + error);
@@ -11738,6 +12710,7 @@ ${err.stack}` : "");
$("#subtitleSearchBtn").on("click", (() => {
window.JavPackSubtitle.openSearchModal({details: {code: carNum2}});
}));
+ window.JavPackSubtitle.preload115Matches(carNum2);
let tempCarNum = carNum2.replace("FC2-", "");
this.getBean("OtherSitePlugin").loadOtherSite(tempCarNum, carNum2).then();
}
@@ -11790,7 +12763,7 @@ ${err.stack}` : "");
$(".screen-container").on("click", ((event) => {
event.stopPropagation();
event.preventDefault();
- showImageViewer(event.currentTarget);
+ showPreviewOverlay(imgUrl, carNum2);
}));
}
}));
@@ -11952,6 +12925,7 @@ ${err.stack}` : "");
$("#subtitleSearchBtn").on("click", (() => {
window.JavPackSubtitle.openSearchModal({details: {code: carNum2}});
}));
+ window.JavPackSubtitle.preload115Matches(carNum2);
this.showStatus(carNum2).then();
}
async showStatus(carNum2) {
diff --git a/src/core/constants.js b/src/core/constants.js
index e109cff..fc792ba 100644
--- a/src/core/constants.js
+++ b/src/core/constants.js
@@ -451,6 +451,124 @@ const mainCss = `
background: rgba(0, 0, 0, 0.3);
z-index: 999998;
}
+
+ .jhs-preview-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.85);
+ z-index: 2147483647;
+ display: flex;
+ overflow: auto;
+ cursor: zoom-out;
+ backdrop-filter: blur(5px);
+ }
+ .jhs-preview-img {
+ border-radius: 4px;
+ margin: auto;
+ cursor: zoom-in;
+ max-width: 95vw;
+ max-height: 95vh;
+ object-fit: contain;
+ display: block;
+ box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
+ }
+ .jhs-preview-img.zoomed {
+ max-width: none;
+ max-height: none;
+ cursor: zoom-out;
+ }
+ .jhs-preview-toolbar {
+ position: fixed;
+ top: 20px;
+ right: 20px;
+ display: flex;
+ gap: 8px;
+ z-index: 2147483648;
+ background: #fff;
+ padding: 8px 12px;
+ border-radius: 8px;
+ border: 1px solid #e5e7eb;
+ box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
+ }
+ .jhs-preview-btn {
+ padding: 7px 16px;
+ background: #f3f4f6;
+ color: #374151;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ font-size: 13px;
+ font-weight: 500;
+ cursor: pointer;
+ transition: all 0.2s ease;
+ display: inline-flex;
+ align-items: center;
+ gap: 4px;
+ white-space: nowrap;
+ }
+ .jhs-preview-btn:hover {
+ background: #e5e7eb;
+ border-color: #9ca3af;
+ }
+ .jhs-preview-btn.active {
+ color: #fff;
+ background: #2ecc71;
+ border-color: #2ecc71;
+ }
+ .jhs-loading-overlay {
+ position: fixed;
+ inset: 0;
+ background: rgba(0, 0, 0, 0.7);
+ z-index: 2147483649;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #fff;
+ font-size: 18px;
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
+ }
+ .jhs-loading-spinner {
+ text-align: center;
+ }
+ .jhs-loading-spinner .spinner {
+ width: 40px;
+ height: 40px;
+ margin: 0 auto 12px;
+ border: 3px solid rgba(255, 255, 255, 0.3);
+ border-top-color: #fff;
+ border-radius: 50%;
+ animation: jhs-spin 0.6s linear infinite;
+ }
+ @keyframes jhs-spin {
+ to { transform: rotate(360deg); }
+ }
+ .jhs-preview-toast {
+ position: fixed;
+ top: 80px;
+ left: 50%;
+ transform: translate(-50%, -20px);
+ z-index: 2147483650;
+ padding: 10px 24px;
+ border-radius: 8px;
+ font-size: 14px;
+ font-weight: 600;
+ font-family: -apple-system, BlinkMacSystemFont, sans-serif;
+ color: #fff;
+ opacity: 0;
+ transition: opacity 0.3s ease, transform 0.3s ease;
+ pointer-events: none;
+ }
+ .jhs-preview-toast.show {
+ opacity: 1;
+ transform: translate(-50%, 0);
+ }
+ .jhs-preview-toast-error {
+ background: rgba(220, 38, 38, 0.9);
+ box-shadow: 0 4px 16px rgba(220, 38, 38, 0.4);
+ }
+ .jhs-preview-toast-info {
+ background: rgba(59, 130, 246, 0.9);
+ box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4);
+ }
`;
diff --git a/src/plugins/cover-button.js b/src/plugins/cover-button.js
index 805ddbf..a7085f8 100644
--- a/src/plugins/cover-button.js
+++ b/src/plugins/cover-button.js
@@ -101,7 +101,7 @@ class CoverButtonPlugin extends BasePlugin {
carNum2 = carNum2.replace("FC2-", "");
const imgUrl = await this.getBean("ScreenShotPlugin").getScreenshot(carNum2);
loadObj.close();
- showImageViewer(imgUrl);
+ showPreviewOverlay(imgUrl, carNum2);
} catch (error) {
console.error("图片预览出错:", error);
show.error("图片预览出错:" + error);
diff --git a/src/plugins/fc2-by123av.js b/src/plugins/fc2-by123av.js
index 04e4272..2174ecf 100644
--- a/src/plugins/fc2-by123av.js
+++ b/src/plugins/fc2-by123av.js
@@ -269,7 +269,7 @@ class Fc2By123AvPlugin extends BasePlugin {
$(".screen-container").on("click", (event => {
event.stopPropagation();
event.preventDefault();
- showImageViewer(event.currentTarget);
+ showPreviewOverlay(imgUrl, carNum2);
}));
}
}));
diff --git a/src/plugins/fc2.js b/src/plugins/fc2.js
index 3353678..e5ccbad 100644
--- a/src/plugins/fc2.js
+++ b/src/plugins/fc2.js
@@ -151,7 +151,7 @@ class Fc2Plugin extends BasePlugin {
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);
+ imgUrl && await screenShotPlugin.addImg("缩略图", imgUrl, carNum2);
}));
}
handleMagnets(movieId) {
diff --git a/src/plugins/screenshot.js b/src/plugins/screenshot.js
index 2a34a7e..5599be9 100644
--- a/src/plugins/screenshot.js
+++ b/src/plugins/screenshot.js
@@ -1,153 +1,492 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb } from '../core/constants.js';
-import { searchFiles } from './wangpan-115-task.js';
+
+const Thumbnail = {
+ sources: ['javfree', 'projectjav', 'javstore'],
+
+ cacheKey(code) {
+ return `thumb_cache_v3_${code}`;
+ },
+
+ lookupCode(code) {
+ const text = String(code || '').trim();
+ const fc2 = text.match(/^(?:FC2[-_\s]?(?:PPV[-_\s]?)?)?(\d{6,9})$/i);
+ return fc2 ? fc2[1] : text;
+ },
+
+ sourceOrder() {
+ const savedOrder = ['javfree', 'projectjav', 'javstore'];
+ const seen = new Set();
+ return [...savedOrder, ...this.sources].filter(src => {
+ if (seen.has(src) || typeof this[src] !== 'function') return false;
+ seen.add(src);
+ return true;
+ });
+ },
+
+ async fetchFromSource(source, code) {
+ try {
+ return await this[source](this.lookupCode(code));
+ } catch (e) {
+ clog.debug(`Thumbnail[${source}] 异常:`, e.message);
+ return null;
+ }
+ },
+
+ normalizeForCompare(text) {
+ return String(text || '').toLowerCase().replace(/[^a-z0-9]/g, '');
+ },
+
+ isCodeMatched(text, code) {
+ const normalizedText = this.normalizeForCompare(text);
+ const normalizedCode = this.normalizeForCompare(code);
+ return !!normalizedCode && normalizedText.includes(normalizedCode);
+ },
+
+ isDetailMatched(doc, url, code) {
+ const title = doc?.querySelector('title')?.textContent || '';
+ const headings = [...(doc?.querySelectorAll('h1,h2,h3,.entry-title,.movie-title,.post-title') || [])]
+ .map(el => el.textContent || '').join(' ');
+ const bodyText = (doc?.body?.textContent || '').slice(0, 5000);
+ return this.isCodeMatched([url, title, headings, bodyText].join(' '), code);
+ },
+
+ normalizePreviewUrl(url, baseUrl = '') {
+ if (!url) return '';
+ const absolute = /^https?:\/\//i.test(url)
+ ? url : (baseUrl ? new URL(url, baseUrl).href : url);
+ return absolute.replace(/^http:/, 'https:');
+ },
+
+ isJavfreePreviewImage(url, code) {
+ const cleanUrl = String(url || '').split('?')[0];
+ const lookupCode = this.lookupCode(code);
+ const isFc2Numeric = /^\d{6,9}$/.test(lookupCode);
+ const fc2ShotPattern = isFc2Numeric
+ ? new RegExp(`${lookupCode}_\\d+\\.(?:jpe?g|png|webp)$`, 'i')
+ : null;
+ return this.isCodeMatched(cleanUrl, code) && (
+ /-(?:1080p|demosaic)\.(?:jpe?g|png|webp)$/i.test(cleanUrl) ||
+ (isFc2Numeric && fc2ShotPattern.test(cleanUrl))
+ );
+ },
+
+ selectJavfreePreviewUrl(doc, detailUrl, code) {
+ const urls = [...doc.querySelectorAll('p > img[src]')]
+ .map(img => this.normalizePreviewUrl(img.getAttribute('src') || img.src || '', detailUrl))
+ .filter(url => this.isJavfreePreviewImage(url, code));
+ return urls.find(url => /-1080p\./i.test(url)) ||
+ urls.find(url => /-demosaic\./i.test(url)) ||
+ urls.find(url => /_1\.(?:jpe?g|png|webp)$/i.test(url)) || '';
+ },
+
+ async javfree(code) {
+ code = this.lookupCode(code);
+ const cacheKey = this.cacheKey(code);
+ const cached = sessionStorage.getItem(cacheKey);
+ if (cached) return cached;
+
+ try {
+ const html = await gmHttp.get(`https://javfree.me/search/${code}`);
+ const doc = new DOMParser().parseFromString(html, 'text/html');
+ const link = [...doc.querySelectorAll('.entry-title>a')]
+ .find(a => this.isCodeMatched([a.href, a.textContent].join(' '), code))?.href;
+ if (!link) return null;
+
+ const dHtml = await gmHttp.get(link);
+ const dDoc = new DOMParser().parseFromString(dHtml, 'text/html');
+ if (!this.isDetailMatched(dDoc, link, code)) return null;
+
+ const url = this.selectJavfreePreviewUrl(dDoc, link, code);
+ if (url) {
+ sessionStorage.setItem(cacheKey, url);
+ return url;
+ }
+ return null;
+ } catch {
+ return null;
+ }
+ },
+
+ async javstore(code) {
+ code = this.lookupCode(code);
+ try {
+ const normalizedCode = code.replace(/^fc2-?/i, '').replace(/-/g, '').toLowerCase();
+ const searchUrl = `https://javstore.net/search?q=${encodeURIComponent(code)}`;
+ const searchHtml = await gmHttp.get(searchUrl);
+ const searchDoc = new DOMParser().parseFromString(searchHtml, 'text/html');
+
+ const candidateLinks = searchDoc.querySelectorAll('a[href*="/"]');
+ const detailUrls = [];
+ for (const link of candidateLinks) {
+ const href = link.getAttribute('href');
+ if (!href) continue;
+ if (href.startsWith('http') && !href.includes('javstore.net')) continue;
+ const urlObj = new URL(href, searchUrl);
+ if (!/javstore\.net$/i.test(urlObj.hostname)) continue;
+ if (/^\/search(?:[/?#]|$)/i.test(urlObj.pathname)) continue;
+ const fullUrl = urlObj.href;
+ const pathLastPart = decodeURIComponent(urlObj.pathname.split('/').pop() || '');
+ const normalizedPath = pathLastPart.toLowerCase().replace(/-/g, '');
+ const looksLikeDetail = /\.html$/i.test(urlObj.pathname) || /^\/\d+[-/]/.test(urlObj.pathname);
+ if (looksLikeDetail && normalizedPath.includes(normalizedCode) && !detailUrls.includes(fullUrl)) {
+ detailUrls.push(fullUrl);
+ }
+ }
+ if (detailUrls.length === 0) return null;
+
+ for (const detailUrl of detailUrls) {
+ const imgUrl = await this._extractImgFromDetail(detailUrl, code);
+ if (imgUrl) return imgUrl;
+ }
+ return null;
+ } catch (e) {
+ clog.debug('javstore 获取失败', e);
+ return null;
+ }
+ },
+
+ async _extractImgFromDetail(detailUrl, code) {
+ try {
+ const detailHtml = await gmHttp.get(detailUrl);
+ const detailDoc = new DOMParser().parseFromString(detailHtml, 'text/html');
+ if (!this.isDetailMatched(detailDoc, detailUrl, code)) return null;
+
+ for (const link of detailDoc.querySelectorAll('a')) {
+ if (link.textContent.includes('CLICK HERE')) {
+ const imgUrl = link.href || link.getAttribute('href') || '';
+ if (imgUrl) return this.normalizePreviewUrl(imgUrl, detailUrl);
+ }
+ }
+
+ const img = detailDoc.querySelector('img[src*="_s.jpg"]');
+ if (img) {
+ let src = img.getAttribute('src') || '';
+ if (!src.startsWith('http')) src = new URL(src, detailUrl).href;
+ return this.normalizePreviewUrl(src.replace(/_s\.jpg$/, '_l.jpg'), detailUrl);
+ }
+ return null;
+ } catch (e) {
+ clog.debug('javstore: 详情页请求失败', detailUrl, e.message);
+ return null;
+ }
+ },
+
+ async projectjav(code) {
+ code = this.lookupCode(code);
+ try {
+ const request = (url) => gmHttp.get(url, {}, {
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
+ 'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
+ });
+
+ const searchUrl = `https://projectjav.com/?searchTerm=${encodeURIComponent(code)}`;
+ const searchHtml = await request(searchUrl);
+ const searchDoc = new DOMParser().parseFromString(searchHtml, 'text/html');
+
+ let detailUrl = '';
+ const allMovieLinks = [...searchDoc.querySelectorAll('a[href*="/movie/"]')];
+ const firstLink = allMovieLinks[0]?.getAttribute('href') || '';
+ if (firstLink) {
+ detailUrl = firstLink.startsWith('http') ? firstLink : `https://projectjav.com${firstLink}`;
+ }
+ if (!detailUrl) return null;
+
+ const detailHtml = await request(detailUrl);
+ const detailDoc = new DOMParser().parseFromString(detailHtml, 'text/html');
+
+ const screenshotLink = [...detailDoc.querySelectorAll('.col-md-12.thumbnail a[data-featherlight="image"], .thumbnail a[data-featherlight="image"]')]
+ .find(a => this.isCodeMatched([a.outerHTML, a.closest('.thumbnail')?.outerHTML, detailUrl].join(' '), code));
+
+ if (screenshotLink) {
+ const thumbImg = screenshotLink.querySelector('img');
+ const href = screenshotLink.getAttribute('href') || '';
+ if (href) return this.normalizePreviewUrl(href, detailUrl);
+ if (thumbImg) {
+ const src = (thumbImg.getAttribute('src') || '').replace(/\?.*$/, '');
+ if (src) return this.normalizePreviewUrl(src, detailUrl);
+ }
+ }
+ return null;
+ } catch (e) {
+ clog.debug('[projectjav] 异常:', e.message);
+ return null;
+ }
+ },
+
+ async get(code) {
+ const cacheKey = this.cacheKey(code);
+ const cached = sessionStorage.getItem(cacheKey);
+ if (cached) return {url: cached, source: null};
+
+ let url = null, source = null;
+ for (const src of this.sourceOrder()) {
+ url = await this.fetchFromSource(src, code);
+ if (url) {
+ source = src;
+ break;
+ }
+ }
+
+ if (url) sessionStorage.setItem(cacheKey, url);
+ return {url, source};
+ }
+};
+
+let previewOverlay = null;
+
+function showPreviewOverlay(imgUrl, code, source = null) {
+ if (previewOverlay) {
+ previewOverlay.close();
+ }
+ let currentBlobUrl = null;
+
+ const originalHtmlOverflow = document.documentElement.style.overflow;
+ const originalBodyOverflow = document.body.style.overflow;
+ document.documentElement.style.overflow = 'hidden';
+ document.body.style.overflow = 'hidden';
+
+ const container = document.createElement('div');
+ container.className = 'jhs-preview-overlay';
+
+ const img = document.createElement('img');
+ img.src = imgUrl;
+ img.className = 'jhs-preview-img';
+ img.onclick = (e) => {
+ e.stopPropagation();
+ img.classList.toggle('zoomed');
+ };
+ img.onerror = () => {
+ img.src = imgUrl;
+ };
+
+ const toolbar = document.createElement('div');
+ toolbar.className = 'jhs-preview-toolbar';
+
+ const createButton = (text, className, onClick) => {
+ const btn = document.createElement('button');
+ btn.className = `jhs-preview-btn ${className}`;
+ btn.textContent = text;
+ btn.onclick = onClick;
+ return btn;
+ };
+
+ const setActiveSource = (activeSource) => {
+ toolbar.querySelectorAll('.jhs-preview-btn').forEach(b => b.classList.remove('active'));
+ const activeBtn = toolbar.querySelector(`.jhs-preview-btn-${activeSource}`);
+ if (activeBtn) activeBtn.classList.add('active');
+ };
+
+ const changeSource = async (src, srcName) => {
+ const loadingOverlay = document.createElement('div');
+ loadingOverlay.className = 'jhs-loading-overlay';
+ loadingOverlay.innerHTML = '
';
+ document.body.appendChild(loadingOverlay);
+ try {
+ const newUrl = src === 'javfree' ? await Thumbnail.javfree(code)
+ : src === 'projectjav' ? await Thumbnail.projectjav(code)
+ : await Thumbnail.javstore(code);
+ if (newUrl) {
+ if (currentBlobUrl) {
+ URL.revokeObjectURL(currentBlobUrl);
+ currentBlobUrl = null;
+ }
+ img.src = newUrl;
+ setActiveSource(src);
+ } else {
+ showToast(`${srcName} 未找到预览图`, 'error');
+ }
+ } catch (err) {
+ showToast(`${srcName} 加载失败: ${err.message}`, 'error');
+ } finally {
+ loadingOverlay.remove();
+ }
+ };
+
+ let toastTimer = null;
+ const showToast = (msg, type = 'info') => {
+ const existing = container.querySelector('.jhs-preview-toast');
+ if (existing) existing.remove();
+ if (toastTimer) clearTimeout(toastTimer);
+
+ const toast = document.createElement('div');
+ toast.className = `jhs-preview-toast jhs-preview-toast-${type}`;
+ toast.textContent = msg;
+ container.appendChild(toast);
+
+ requestAnimationFrame(() => toast.classList.add('show'));
+ toastTimer = setTimeout(() => {
+ toast.classList.remove('show');
+ setTimeout(() => toast.remove(), 300);
+ }, 3000);
+ };
+
+ const javfreeBtn = createButton('javfree', 'jhs-preview-btn-javfree javfree', (e) => {
+ e.stopPropagation();
+ changeSource('javfree', 'javfree');
+ });
+
+ const projectjavBtn = createButton('projectjav', 'jhs-preview-btn-projectjav projectjav', (e) => {
+ e.stopPropagation();
+ changeSource('projectjav', 'projectjav');
+ });
+
+ const javstoreBtn = createButton('javstore', 'jhs-preview-btn-javstore javstore', (e) => {
+ e.stopPropagation();
+ changeSource('javstore', 'javstore');
+ });
+
+ const newWindowBtn = createButton('新窗口打开', 'jhs-preview-btn-action action', (e) => {
+ e.stopPropagation();
+ window.open(img.src);
+ });
+
+ const downloadBtn = createButton('下载', 'jhs-preview-btn-action action', (e) => {
+ e.stopPropagation();
+ const a = document.createElement('a');
+ a.href = img.src;
+ a.download = `${code}.jpg`;
+ a.click();
+ });
+
+ toolbar.appendChild(javfreeBtn);
+ toolbar.appendChild(projectjavBtn);
+ toolbar.appendChild(javstoreBtn);
+ toolbar.appendChild(newWindowBtn);
+ toolbar.appendChild(downloadBtn);
+
+ if (source === 'javfree') setActiveSource('javfree');
+ else if (source === 'projectjav') setActiveSource('projectjav');
+ else if (source === 'javstore') setActiveSource('javstore');
+ else setActiveSource('javfree');
+
+ container.appendChild(img);
+
+ const closeOverlay = () => {
+ if (container.parentNode) {
+ container.remove();
+ toolbar.remove();
+ document.documentElement.style.overflow = originalHtmlOverflow;
+ document.body.style.overflow = originalBodyOverflow;
+ if (currentBlobUrl) {
+ URL.revokeObjectURL(currentBlobUrl);
+ currentBlobUrl = null;
+ }
+ }
+ };
+
+ container.onclick = closeOverlay;
+
+ const escHandler = (e) => {
+ if (e.key === 'Escape') {
+ closeOverlay();
+ document.removeEventListener('keydown', escHandler);
+ }
+ };
+ document.addEventListener('keydown', escHandler);
+
+ document.body.appendChild(container);
+ document.body.appendChild(toolbar);
+ previewOverlay = {container, toolbar, close: closeOverlay};
+}
class ScreenShotPlugin extends BasePlugin {
getName() {
return "ScreenShotPlugin";
}
+
async handle() {
this.loadScreenShot().then();
}
+
async loadScreenShot() {
if (!window.isDetailPage) return;
if ("yes" !== await storageManager.getSetting("enableLoadScreenShot", "yes")) return;
let carNum2 = this.getPageInfo().carNum;
- isJavDb && $(".preview-images .tile-item").first().before('
正在加载缩略图
');
- isJavBus && $("#sample-waterfall .sample-box:first").after('
正在加载缩略图
');
+
+ isJavDb && $(".preview-images .tile-item").first().before(
+ '
' +
+ '正在加载缩略图
'
+ );
+ isJavBus && $("#sample-waterfall .sample-box:first").after(
+ '
' +
+ '正在加载缩略图
'
+ );
+
try {
- const imgUrl = await this.getScreenshot(carNum2);
- this.addImg("缩略图", imgUrl);
- clog.log("加载缩略图:", imgUrl);
+ const result = await Thumbnail.get(carNum2);
+ if (result.url) {
+ this.addImg("缩略图", result.url, carNum2, result.source);
+ clog.log("加载缩略图:", result.url, result.source);
+ } else {
+ this.showErrorFallback(carNum2, new Error("所有来源均未找到预览图"));
+ }
} catch (e) {
this.showErrorFallback(carNum2, e);
}
}
+
async getScreenshot(carNum2) {
- const cacheData = localStorage.getItem("jhs_screenShot") ? JSON.parse(localStorage.getItem("jhs_screenShot")) : {};
- if (cacheData[carNum2]) {
- clog.debug("缓存中存在缩略图:", carNum2, cacheData[carNum2]);
- return cacheData[carNum2];
- }
- let imgUrl;
- try {
- imgUrl = await Promise.any([ this.getJavStoreScreenShot(carNum2) ]);
- } catch (e) {
- clog.error("获取缩略图资源失败:", imgUrl, e);
- throw e;
- }
- if (!imgUrl) {
- this.showErrorFallback(carNum2, null);
- return null;
- }
- const httpsIndex = imgUrl.indexOf("https://");
- -1 !== httpsIndex && (imgUrl = imgUrl.substring(httpsIndex));
- cacheData[carNum2] = imgUrl;
- clog.log("缩略图获取成功:", imgUrl);
- localStorage.setItem("jhs_screenShot", JSON.stringify(cacheData));
- return imgUrl;
+ const result = await Thumbnail.get(carNum2);
+ if (result.url) return result.url;
+ throw new Error("未找到预览图");
}
+
async getJavStoreScreenShot(carNum2) {
- let url = `https://javstore.net/search/${carNum2}.html`;
- clog.log("正在解析缩略图:", url);
- let html = await gmHttp.get(url);
- const $dom = utils.htmlTo$dom(html);
- let detailPageUrl = null;
- $dom.find("#content_news h3 span a").each((function() {
- if ($(this).attr("title").toLowerCase().includes(carNum2.toLowerCase())) {
- detailPageUrl = $(this).attr("href");
- return !1;
- }
- }));
- if (!detailPageUrl) {
- clog.error("JavStore, 查询番号失败:", url);
- return null;
- }
- let detailPageHtml = await gmHttp.get(detailPageUrl);
- const $detailPageDom = utils.htmlTo$dom(detailPageHtml);
- let imgUrl = $detailPageDom.find("a:contains('CLICK HERE')").attr("href") || $detailPageDom.find("img[src*='_s.jpg']").attr("src");
- if (!imgUrl) {
- clog.error("JavStore, 解析预览图失败:", url);
- return null;
- }
- return imgUrl.replace(".th", "");
- }
- async getJavBestScreenShot(carNum2) {
- let url = `https://javbest.net/?s=${carNum2}`;
- clog.log("正在解析缩略图:", url);
- let html = await gmHttp.get(url);
- const $dom = utils.htmlTo$dom(html), href = $dom.find(".app_loop_thumb a").first().attr("href");
- if (!href) {
- clog.error("解析JavBest搜索页失败:", url);
- throw new Error("解析JavBest搜索页失败");
- }
- const title = $dom.find(".app_loop_thumb a").first().attr("title");
- if (!title.toLowerCase().includes(carNum2.toLowerCase())) {
- clog.error("解析JavBest搜索页失败:", title);
- throw new Error("解析JavBest搜索页失败");
- }
- const detailPageHtml = await gmHttp.get(href);
- let imgUrl = $(detailPageHtml).find('#content a img[src*="_t.jpg"]').attr("src");
- if (!imgUrl) {
- clog.error("解析JavBest缩略图失败:", url);
- throw new Error("解析JavBest缩略图失败");
- }
- imgUrl = imgUrl.replace("_t", "").replace("http:", "https:");
- return imgUrl;
+ return await Thumbnail.javstore(carNum2);
}
+
async getJavFreeScreenShot(carNum2) {
- let url = `https://javfree.me/search/${carNum2}/`, html = await gmHttp.get(url);
- const $itemList = utils.htmlTo$dom(html).find("article h2.entry-title a");
- if (!$itemList || 0 === $itemList.length) {
- clog.error("解析JavFree搜索页失败:", url);
- throw new Error("解析JavFree搜索页失败");
- }
- let detailPageUrl = $($itemList[0]).attr("href"), detailPageHtml = await gmHttp.get(detailPageUrl);
- const $imgList = utils.htmlTo$dom(detailPageHtml).find("#main > article > .entry-content > p img");
- if (!$imgList || 0 === $imgList.length) {
- clog.error("解析JavFree详情页失败:", detailPageUrl);
- throw new Error("解析JavFree详情页失败");
- }
- const srcList = $imgList.filter((function() {
- const src = $(this).attr("src");
- return src && src.toLowerCase().endsWith(".jpeg");
- })).map((function() {
- return $(this).attr("src");
- })).get();
- console.log(srcList);
- return srcList.at(-1);
+ return await Thumbnail.javfree(carNum2);
}
- addImg(title, imgUrl) {
- if (imgUrl) {
- isJavDb && $(".screen-container").html(`

`);
- isJavBus && $(".screen-container").html(`
`);
- $(".screen-container").on("click", (event => {
- event.stopPropagation();
- event.preventDefault();
- showImageViewer(event.currentTarget);
- }));
- }
+
+ addImg(title, imgUrl, code, source) {
+ if (!imgUrl) return;
+ isJavDb && $(".screen-container").html(
+ `

`
+ );
+ isJavBus && $(".screen-container").html(
+ `
`
+ );
+ $(".screen-container").on("click", (event) => {
+ event.stopPropagation();
+ event.preventDefault();
+ showPreviewOverlay(imgUrl, code || title, source);
+ });
}
+
showErrorFallback(carNum2, error) {
- var _a2;
- console.error("获取缩略图失败:", null == (_a2 = null == error ? void 0 : error.message) ? void 0 : _a2.substring(0, 100));
- let differentCss = isJavBus ? "margin-top: 30px" : "margin-top: 50px";
- $(".screen-container").html(`
获取缩略图失败
点击重试 或
前往确认`).off("click", ".retry-link").off("click", ".check-link").on("click", ".retry-link", (async e => {
+ console.error("获取缩略图失败:", error?.message?.substring?.(0, 100) || error);
+ let differentCss = isJavBus ? "margin-top:30px" : "margin-top:50px";
+ $(".screen-container").html(
+ `
获取缩略图失败
` +
+ `
点击重试 或 ` +
+ `
前往确认`
+ ).off("click", ".retry-link").off("click", ".check-link")
+ .on("click", ".retry-link", async (e) => {
e.stopPropagation();
e.preventDefault();
- $(".screen-container").html(`
正在重新加载...
`);
+ $(".screen-container").html(
+ `
正在重新加载...
`
+ );
try {
- const imgUrl = await this.getScreenshot(carNum2);
- this.addImg("缩略图", imgUrl);
+ const result = await Thumbnail.get(carNum2);
+ if (result.url) {
+ this.addImg("缩略图", result.url, carNum2, result.source);
+ } else {
+ this.showErrorFallback(carNum2, new Error("所有来源均未找到预览图"));
+ }
} catch (err) {
this.showErrorFallback(carNum2, err);
}
- })).on("click", ".check-link", (async e => {
+ }).on("click", ".check-link", (e) => {
e.stopPropagation();
e.preventDefault();
- window.open(`https://javstore.net/search/${carNum2}.html`, "_blank");
- }));
+ window.open(`https://javstore.net/search?q=${encodeURIComponent(carNum2)}`, "_blank");
+ });
}
}
-export { ScreenShotPlugin };
\ No newline at end of file
+export {ScreenShotPlugin, Thumbnail, showPreviewOverlay};
+window.showPreviewOverlay = showPreviewOverlay;
\ No newline at end of file