16 Commits

Author SHA1 Message Date
FireInRain 2b8c925fed Merge pull request #2 from anupamme/fix-repo-jhs-enhance-remove-hardcoded-imgur-client-id
fix: the userscript embeds imgur api credentials dir... in...
2026-08-31 21:42:16 +08:00
lzyme.dev@gmail.com a9e5286c53 更新文档 2026-08-31 14:31:53 +08:00
anupamme 7005b528a3 fix: V-001 security vulnerability
Automated security fix generated by OrbisAI Security
2026-08-31 06:14:02 +00:00
lzyme.dev@gmail.com 4026b84fc2 更新版本3.3.10 2026-08-31 14:04:44 +08:00
lzyme.dev@gmail.com 76b608ccc0 调整UI 2026-08-31 14:00:58 +08:00
lzyme.dev@gmail.com 6c9a4743bb 新增验车功能 2026-08-31 13:50:35 +08:00
lzyme.dev@gmail.com ce6cedf164 更新磁力搜索 2026-08-31 13:32:56 +08:00
lzyme.dev@gmail.com 7840ca6389 磁力搜索磁力新增排序 2026-08-31 11:49:58 +08:00
lzyme.dev@gmail.com d03a9d1462 修复磁力搜索里面点击下载触发重复请求 2026-08-31 11:31:20 +08:00
lzyme.dev@gmail.com b7cfc8541c update 2026-08-31 08:38:26 +08:00
lzyme.dev@gmail.com ce59d82ed2 更新文档和版本 2026-08-30 22:18:06 +08:00
lzyme.dev@gmail.com 37cf4e1183 新增进入详情页面对不规则名文件进行重命名 2026-08-30 22:15:38 +08:00
lzyme.dev@gmail.com 8829af0fc3 修复不规则vr作品名字无法匹配 2026-08-30 21:43:56 +08:00
lzyme.dev@gmail.com 9db06fba3b 修复layui悬浮窗错误不加载 2026-08-30 20:33:17 +08:00
lzyme.dev@gmail.com 06b2497ee1 修复115固有jq导致加载冲突 2026-08-30 20:10:07 +08:00
lzyme.dev@gmail.com 50581be5a6 修复在115首页导致jq加载冲突 2026-08-30 19:42:34 +08:00
11 changed files with 16659 additions and 13187 deletions
+24 -5
View File
@@ -36,6 +36,9 @@
### 详情页增强
- **磁力搜索聚合(MagnetHub)**:多引擎聚合搜索(Sukebei / U9A9 / U3C3 / BTSearch / SoKitty / ØMagnet),Tab
切换引擎,结果表格展示(名称/大小/日期/操作),支持按大小/最新/最旧排序,质量标签自动识别(中字/4K/破解),一键复制磁力链接(含番号提取码),验车功能(Whatslink
API 截图预览、键盘/缩放导航),115 离线下载快捷按钮
- **磁力排序与过滤(JavDB)**:按发布日期/文件大小/文件数量拖拽排序,勾选启用;高清/4K/字幕/无码多条件并集过滤,4K 磁力行高亮显示
- 磁力链接高亮匹配已收藏/已下载状态
- DMM 多画质预览视频
@@ -48,11 +51,17 @@
### 115 网盘集成
- **番号智能匹配**:双阶段搜索策略 — 第一阶段精准搜索(完整番号 + `type:4`),未命中时第二阶段模糊兜底(尾号 +
正则匹配),支持不规则文件名(如 `MIVR00345-5.mp4`、`ABP001.mp4`)
- **磁力离线下载**:MagnetHub 磁力搜索结果支持一键添加至 115 离线下载
- **不规则文件名自动重命名**
:检测到需第二阶段才能匹配的作品时,自动将文件重命名为标准番号格式(如 `mdvr00317-3.mp4` → `MDVR-317-3.mp4`
),设置面板可开关 + 选择范围(仅VR / 全部)
- 作品目录匹配与详情页预加载
- 多目录选择上传字幕
- 字幕一键直传至匹配目录
- **作品删除增强**:删除整个文件夹(非仅视频),支持标记删除批量操作,删除后字幕目录列表联动;批量删除含风控保护(1~3s
随机延迟 + 触发风控自动重试退避)
随机延迟 + 触发风控自动重试退避);删除安全检查使用 `codeParse` 正则匹配番号
### 数据管理
@@ -70,7 +79,8 @@
### 其他
- FC2 站点支持(含 123AV 补全)
- MagnetHub 磁力搜索
- **MagnetHub 磁力搜索**:6 引擎聚合(Sukebei/U9A9/U3C3/BTSearch/SoKitty/ØMagnet),质量标签(中字/4K/破解),验车截图预览(Whatslink
API,键盘导航),排序切换,115 离线一键下载
- JavTrailers 站点处理
- 设置面板:快捷键配置、域名配置、缓存清理、标记删除列表、打赏作者(二维码/钱包地址可自定义)
- 浏览历史、想看/已看列表
@@ -108,7 +118,7 @@ JHS-enhance/
│ ├── list-page-button.js # 列表页功能按钮(待鉴定/已收藏/新作品/黑名单管理)
│ ├── detail-page.js # 详情页数据加载与基础增强
│ ├── detail-page-button.js# 详情页操作按钮(115 目录预加载 + 磁力排序过滤面板)
│ ├── setting.js # 设置面板(Tab 页:基础/任务/域名/快捷键/屏蔽/缓存/标记删除/打赏作者)
│ ├── setting.js # 设置面板(Tab 页:基础/任务/域名/快捷键/屏蔽/缓存/标记删除/打赏作者;含 115 自动重命名开关 + 范围选择)
│ ├── translate.js # 标题翻译(Google 翻译 API,原始标题缓存)
│ ├── preview-video.js # DMM 预览视频播放器
│ ├── highlight-magnet.js # 磁力链接高亮 + 排序 + 过滤(多键排序、并集过滤、4K 高亮)
@@ -118,7 +128,7 @@ JHS-enhance/
│ ├── new-video.js # 新作品检测与通知
│ ├── task.js # 后台任务调度(黑名单/收藏演员/新作品定时执行)
│ ├── history.js # 浏览历史表格
│ ├── wangpan-115.js # 115 网盘匹配/标签/删除(含风控保护的批量删除)
│ ├── wangpan-115.js # 115 网盘匹配/标签/删除(含 codeParse 正则匹配、双阶段搜索、自动重命名)
│ ├── wangpan-115-task.js # 115 网盘任务处理(文件搜索 API 封装)
│ ├── aliyun-pan.js # 阿里云盘备份/恢复
│ ├── fc2.js # FC2 页面处理
@@ -139,7 +149,7 @@ JHS-enhance/
│ ├── screenshot.js # 多源预览图(javfree/projectjav/javstore + 来源切换弹窗)
│ ├── subtitles.js # 字幕搜索(整合迅雷+SubTitleCat)+ 115 多目录直传
│ ├── req115.js # 115 网盘 API 封装(Req 基类 + Drive115 + Req115)
│ ├── magnet-hub.js # MagnetHub 磁力搜索
│ ├── magnet-hub.js # MagnetHub 磁力搜索(6 引擎聚合、质量标签、验车截图、115 离线)
│ ├── other-site.js # 第三方站点跳转链接
│ ├── jav-trailers.js # JavTrailers 站点处理
│ ├── javxy-preview-video.js# Javxy 多源视频预览(独立按钮、聚合 API、HLS 支持)
@@ -297,6 +307,15 @@ window.show.ok('操作成功'); // Toast 通知
## 版本历史
- **v3.3.9**
- 新增:115 番号智能匹配 — `codeParse` 正则匹配引擎,双阶段搜索策略(精准 →
模糊兜底),支持不规则文件名(如 `MIVR00345-5.mp4`、`ABP001.mp4`)
- 新增:115 不规则文件名自动重命名 —
检测到需模糊匹配的作品时自动批量重命名为标准格式(如 `mdvr00317-3.mp4` → `MDVR-317-3.mp4`),设置面板可开关 +
范围选择(仅VR / 全部)
- 优化:删除安全检查改用 `codeParse` 正则匹配,消除 `includes` 方式对不规则文件名的误判
- 优化:`searchFiles` 增加 `type:4` 参数,搜索仅返回视频文件,避免目录占用 30 条槽位
- **v3.3.8**
- 新增:Javxy 多源视频预览独立按钮(番号/识别码右侧),基于 Javxy 聚合 API,支持 JavTrailers / JavDB 等多源回退
- 新增:HLS 视频播放运行时(hls.js 封装),支持 .m3u8 格式、播放进度记忆、画质切换
+15430 -12979
View File
File diff suppressed because one or more lines are too long
+4 -3
View File
@@ -12,6 +12,9 @@ if (typeof $.trim !== 'function') {
return text == null ? '' : (text + '').trim();
};
}
if (typeof jQuery !== 'undefined' && typeof layer === 'undefined') {
await import('layui-layer');
}
window.localforage = localforage;
window.Toastify = Toastify;
window.md5 = md5;
@@ -31,6 +34,4 @@ window.Status_FAVORITE = Status_FAVORITE;
window.Status_HAS_DOWN = Status_HAS_DOWN;
window.Status_HAS_WATCH = Status_HAS_WATCH;
window.NO = NO;
window.YES = YES;
import 'layui-layer';
window.YES = YES;
+1 -1
View File
@@ -98,7 +98,7 @@ class Utils {
}
if (!viewerExists) {
this.layerIndexStack.pop();
layer.close(topLayerIndex);
typeof layer !== 'undefined' && layer.close(topLayerIndex);
}
}
setupEscClose(layerIndex) {
+23 -21
View File
@@ -820,29 +820,31 @@ window.ImageHoverPreview = class {
}));
}();
const originalLayerClose = layer.close;
if (typeof layer !== 'undefined') {
const originalLayerClose = layer.close;
layer.close = function(index) {
const result = originalLayerClose.call(this, index);
!function(waitTime = 10) {
setTimeout((() => {
const openLayerCount = document.querySelectorAll(".layui-layer-shade").length;
document.documentElement.style.overflow = openLayerCount > 0 ? "hidden" : "";
}), waitTime);
}();
return result;
};
const originalLayerOpen = layer.open;
layer.open = function(options) {
const originalSuccess = (options = options || {}).success;
options.success = function(layero, index) {
"function" == typeof originalSuccess && originalSuccess.call(this, layero, index);
utils.setupEscClose(index);
layer.close = function (index) {
const result = originalLayerClose.call(this, index);
!function (waitTime = 10) {
setTimeout((() => {
const openLayerCount = document.querySelectorAll(".layui-layer-shade").length;
document.documentElement.style.overflow = openLayerCount > 0 ? "hidden" : "";
}), waitTime);
}();
return result;
};
return originalLayerOpen.call(this, options);
};
const originalLayerOpen = layer.open;
layer.open = function (options) {
const originalSuccess = (options = options || {}).success;
options.success = function (layero, index) {
"function" == typeof originalSuccess && originalSuccess.call(this, layero, index);
utils.setupEscClose(index);
};
return originalLayerOpen.call(this, options);
};
}
utils.importResource("https://cdn.jsdelivr.net/npm/layui-layer@1.0.9/layer.min.css");
+8 -1
View File
@@ -162,10 +162,17 @@ class ImageRecognitionPlugin extends BasePlugin {
type: mimeType
}), formData = new FormData;
formData.append("image", blob);
const idKey = "jhs_imgurClientId";
let clientId = localStorage.getItem(idKey);
if (!clientId) {
clientId = window.prompt("请输入您自己的Imgur Client-ID(可前往 https://api.imgur.com/oauth2/addclient 免费申请)用于图片上传搜索:");
if (!clientId) throw new Error("未提供Imgur Client-ID,无法上传图片");
localStorage.setItem(idKey, clientId);
}
const response = await fetch("https://api.imgur.com/3/image", {
method: "POST",
headers: {
Authorization: "Client-ID d70305e7c3ac5c6"
Authorization: `Client-ID ${clientId}`
},
body: formData
}), data = await response.json();
+1051 -138
View File
File diff suppressed because it is too large Load Diff
+12 -1
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -6,8 +6,9 @@ const getDownPathList = async () => {
return "object" == typeof res ? res.data : null;
};
const searchFiles = async (search_value, offset = 0, limit = 30) => {
const url = `https://webapi.115.com/files/search?search_value=${encodeURIComponent(search_value)}&offset=${offset}&limit=${limit}`;
const searchFiles = async (search_value, offset = 0, limit = 30, params = {}) => {
const queryParams = new URLSearchParams({search_value, offset, limit, ...params});
const url = `https://webapi.115.com/files/search?${queryParams}`;
return await gmHttp.get(url);
};
+100 -18
View File
@@ -275,7 +275,13 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
const $detail = $('\n <div class="jhs-match-detail jhs-115-match-detail" id="115-match-table">\n <table>\n <thead>\n <tr style="text-align: center">\n <th colspan="4">115匹配</th>\n </tr>\n <tr>\n <th>名称</th>\n <th>大小</th>\n <th>时间</th>\n <th>播放</th>\n </tr>\n </thead>\n <tbody>\n </tbody>\n </table>\n </div>\n '), $tbody = $detail.find("tbody");
try {
const carNum2 = this.getPageInfo().carNum;
matchList || (matchList = await this.searchFiles(carNum2));
if (!matchList) {
const {results, usedPhase2} = await this._searchFilesWithPhase(carNum2);
matchList = results;
if (usedPhase2 && matchList.length > 0) {
await this.autoRenameFiles(carNum2, matchList);
}
}
await this.checkLoginStatus();
if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $tbody.append(`<tr><td colspan="4">\n <a class='jhs-match-no-login-btn a-info'\n data-keyword="${carNum2}"\n title="未登录115网盘">未登录</a>\n </td></tr>`); else if (matchList.length > 0) {
const rowsHtml = matchList.map((match => `\n <tr>\n <td>${match.name}</td>\n <td>${this.formatSize(match.size)}</td>\n <td>${match.createTime}</td>\n <td>\n <a href="https://115vod.com/?pickcode=${match.videoId}&share_id=0"\n target="_blank"\n class="a-success"\n title="播放">播放</a>\n </td>\n </tr>\n `)).join("");
@@ -406,8 +412,8 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
onProgress && onProgress(i + 1, selectedItems.length, item, "未匹配到115文件");
continue;
}
const upperCarNum = item.carNum.toUpperCase();
const safeMatchList = matchList.filter(m => m.name.toUpperCase().includes(upperCarNum));
const {regex: safeRegex} = codeParse(item.carNum);
const safeMatchList = matchList.filter(m => safeRegex.test(m.name));
if (safeMatchList.length === 0) {
failedCount++;
onProgress && onProgress(i + 1, selectedItems.length, item, "无安全可删除目录");
@@ -507,8 +513,8 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
}
showDeleteDialog(carNum2, matchList, afterDelete) {
const upperCarNum = carNum2.toUpperCase();
const safeMatchList = matchList.filter(m => m.name.toUpperCase().includes(upperCarNum));
const {regex: safeRegex} = codeParse(carNum2);
const safeMatchList = matchList.filter(m => safeRegex.test(m.name));
const unsafeCount = matchList.length - safeMatchList.length;
if (safeMatchList.length === 0) {
show.error(`作品 "${carNum2}" 匹配到的 ${matchList.length} 个项目均不含番号,无法安全删除。请手动在115中删除。`);
@@ -616,20 +622,76 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
window.open("https://115.com");
}
async searchFiles(carNum2) {
return (await this._searchFilesWithPhase(carNum2)).results;
}
async _searchFilesWithPhase(carNum2) {
const {regex} = codeParse(carNum2);
const searchKeyword = carNum2.toLowerCase().replace("fc2-", "");
const mapAndFilter = (rawData) => {
return rawData.map((data => ({
folderId: data.fid,
dirId: data.cid,
videoId: data.pc,
name: data.n,
createTime: utils.formatDate(new Date(1e3 * data.te)),
size: data.s,
isVideo: true
}))).filter((x => x.folderId && regex.test(x.name)));
};
var _a2;
let searchKeyword = carNum2.toLowerCase().replace("fc2-", "");
return (null == (_a2 = (await searchFiles(searchKeyword)).data) ? void 0 : _a2.map((data => ({
folderId: data.fid,
dirId: data.cid,
videoId: data.pc,
name: data.n,
createTime: utils.formatDate(new Date(1e3 * data.te)),
size: data.s,
isVideo: [ ".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv" ].some((ext => {
var _a3;
return null == (_a3 = data.n) ? void 0 : _a3.toLowerCase().endsWith(ext);
}))
}))).filter((x => x.folderId && x.isVideo && x.name.toLowerCase().includes(searchKeyword)))) || [];
const preciseData = (null == (_a2 = (await searchFiles(searchKeyword, 0, 30, {type: 4})).data) ? void 0 : _a2) || [];
const preciseResults = mapAndFilter(preciseData);
if (preciseResults.length > 0) return {results: preciseResults, usedPhase2: false};
const numberPart = extractNumberPart(carNum2);
if (numberPart === searchKeyword) return {results: preciseResults, usedPhase2: false};
const fuzzyData = (null == (_a2 = (await searchFiles(numberPart, 0, 30, {type: 4})).data) ? void 0 : _a2) || [];
return {results: mapAndFilter(fuzzyData), usedPhase2: true};
}
buildRenameTarget(carNum, originalName) {
const {regex} = codeParse(carNum);
const match = regex.exec(originalName);
if (!match) return null;
const before = originalName.slice(0, match.index);
const after = originalName.slice(match.index + match[0].length);
return before + carNum + after;
}
async autoRenameFiles(carNum, matchList) {
try {
const enableAutoRename = await storageManager.getSetting("enable115AutoRename", NO);
if (enableAutoRename !== YES) return;
const scope = await storageManager.getSetting("autoRenameScope", "vr_only");
if (scope === "vr_only" && !carNum.toUpperCase().includes("VR")) return;
const renameObj = {};
matchList.forEach(match => {
const newName = this.buildRenameTarget(carNum, match.name);
if (newName && newName !== match.name) {
renameObj[match.folderId] = newName;
}
});
if (Object.keys(renameObj).length === 0) return;
const formData = {};
Object.entries(renameObj).forEach(([fid, newName]) => {
formData[`files_new_name[${fid}]`] = newName;
});
const result = await gmHttp.postForm("https://webapi.115.com/files/batch_rename", formData);
if (result && result.state) {
Object.entries(renameObj).forEach(([fid, newName]) => {
const match = matchList.find(m => m.folderId === fid);
console.log(`[115重命名] ${match.name} → ${newName}`);
});
} else {
console.warn("[115重命名] 重命名失败:", (result && result.error) || "未知错误");
}
} catch (e) {
console.warn("[115重命名] 重命名失败,跳过:", e.message);
}
}
showMatchError($box2, carNum2, error) {
$box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn' data-keyword="${carNum2}" \n title="匹配失败,点击重试">匹配失败</a>`);
@@ -694,5 +756,25 @@ __publicField(_WangPan115MatchPlugin, "LoginStatus", {
let WangPan115MatchPlugin = _WangPan115MatchPlugin;
const extractNumberPart = (carNum) => {
let cleaned = carNum.replace(/^fc2[-_]?(ppv[-_])?/i, "");
const match = cleaned.match(/(\d+)$/);
return match ? match[1] : cleaned;
};
const codeParse = (code) => {
const codes = code.split(/-|_/);
const sep = "\\s?(0|-|_){0,2}\\s?";
let pattern = codes.join(sep);
if (/^fc2/i.test(code)) pattern = `${codes[0]}${sep}(ppv)?${sep}${codes.at(-1)}`;
return {
code,
codes,
prefix: codes[0],
regex: new RegExp(`(?<![a-z])${pattern}(?!\\d)`, "i"),
};
};
export { WangPan115Plugin, WangPan115MatchPlugin };
+3 -18
View File
@@ -8,8 +8,8 @@ export default defineConfig({
userscript: {
name: 'JHS-enhance',
namespace: 'JHS-enhance',
version: '3.3.7',
author: 'xie bro,fireinrain',
version: '3.3.10',
author: 'xiebro,fireinrain',
description: 'Jav-鉴黄师 增强脚本。列表页:作品状态标签、一键筛选、新作品检测、演员黑名单过滤;详情页:磁力链接高亮、DMM 多画质预览视频、标题翻译、整合字幕搜索(迅雷+SubTitleCat)并支持 115 直传、多源预览图(javfree/projectjav/javstore)含来源切换;数据:115 网盘目录匹配与多目录选择、云盘备份/恢复、跨 Tab 同步;其他:自动翻页、分类折叠、Top250、以图识图、热门榜单、评论查看、相关清单。支持 JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers',
license: 'MIT',
icon: 'https://www.google.com/s2/favicons?sz=64&domain=javdb.com',
@@ -92,30 +92,15 @@ export default defineConfig({
build: {
fileName: 'jhs-enhance.user.js',
externalGlobals: {
'jquery': '$',
'jquery': 'jQuery',
'localforage': 'localforage',
'toastify-js': 'Toastify',
'blueimp-md5': 'md5',
'tabulator-tables': 'Tabulator',
'viewerjs': 'Viewer',
'qrcodejs': 'QRCode',
'layui-layer': 'layer',
},
},
}),
{
name: 'layui-layer-dev-shim',
apply: 'serve',
resolveId(id) {
if (id === 'layui-layer') {
return '\0layui-layer-shim';
}
},
load(id) {
if (id === '\0layui-layer-shim') {
return 'export default window.layer;';
}
},
},
],
});