15 Commits
v3 .. v9

Author SHA1 Message Date
lzyme.dev@gmail.com 2643a3d636 修复删除目录 2026-08-30 04:50:28 +08:00
lzyme.dev@gmail.com 34d34b5200 add missing lib 2026-08-30 04:17:27 +08:00
lzyme.dev@gmail.com cc8ff8082e 修复字幕选择作品文件夹无法展示问题 2026-08-30 04:15:23 +08:00
lzyme.dev@gmail.com 5636ee4f74 修复字幕选择作品文件夹无法展示问题 2026-08-30 04:05:40 +08:00
lzyme.dev@gmail.com b0fe131a03 修复视频长预览图 2026-08-30 03:49:06 +08:00
lzyme.dev@gmail.com 06e9f4e8b4 新增字幕直传115作品目录,支持多目录选择 2026-08-30 03:08:17 +08:00
lzyme.dev@gmail.com 8da3ef2ed5 修复瀑布流下滑删除按钮没有出现 2026-08-30 00:38:30 +08:00
lzyme.dev@gmail.com c645eb6629 瀑布流页面可以就行排序 2026-08-30 00:06:07 +08:00
lzyme.dev@gmail.com 0885b5e0fe 新增标记删除 2026-08-29 22:02:02 +08:00
lzyme.dev@gmail.com 268883ac02 强化目录删除的安全边界 保证只删除出现番号匹配的目录 2026-08-29 19:37:55 +08:00
lzyme.dev@gmail.com b1772160a1 详情页也添加删除按钮 2026-08-29 18:59:43 +08:00
lzyme.dev@gmail.com 39f252fa0b 多目录可选删除,暂时无法删除下载记录 2026-08-29 18:44:16 +08:00
lzyme.dev@gmail.com d99af94e74 新增作品列表下删除作品 2026-08-29 18:10:41 +08:00
lzyme.dev@gmail.com 2383406ede 新增作品列表下删除作品 2026-08-29 18:04:57 +08:00
lzyme.dev@gmail.com 8da5c36bf2 更新文档 2026-08-29 16:59:27 +08:00
21 changed files with 5399 additions and 530 deletions
+1 -4
View File
@@ -23,7 +23,6 @@
// @exclude https://*javsee*/*actresses
// @exclude https://*seejav*/forum/*
// @exclude https://*seejav*/*actresses
// @require https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js
// @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
// @require https://cdn.jsdelivr.net/npm/tabulator-tables@6.3.1/dist/js/tabulator.min.js
// @require https://cdn.jsdelivr.net/npm/layui-layer@1.0.9/dist/layer.min.js
@@ -10957,6 +10956,4 @@ pluginManager.processCss().then();
duration: -1
});
pluginManager.processPlugins().then();
}();
}();
+17 -26
View File
@@ -12,52 +12,41 @@
### 一键安装
[![Install](https://img.shields.io/badge/点击安装-JHS--enhance.user.js-brightgreen)](https://github.com/替换为你的用户名/JHS-enhance/releases/latest/download/jhs-enhance.user.js)
[![Install](https://img.shields.io/badge/点击安装-JHS--enhance.user.js-brightgreen)](https://github.com/fireinrain/JHS-enhance/releases/latest/download/jhs-enhance.user.js)
> 点击上方按钮,Tampermonkey 会自动弹出安装提示。安装后脚本会自动更新(Tampermonkey 默认每天检查一次更新)。
### 手动安装
1. 从 [Releases](https://github.com/替换为你的用户名/JHS-enhance/releases) 页面下载最新 `jhs-enhance.user.js`
1. 从 [Releases](https://github.com/fireinrain/JHS-enhance/releases) 页面下载最新 `jhs-enhance.user.js`
2. 打开 Tampermonkey 管理面板 → 拖入文件安装
---
## 自动部署配置
## 自动部署
本项目使用 GitHub Actions 在推送代码时自动构建并发布到 [GitHub Releases](https://github.com/替换为你的用户名/JHS-enhance/releases)。首次使用只需一步:
### 修改 README 中的安装链接
将 README.md 中所有 `替换为你的用户名` 替换为你的 GitHub 用户名:
```
https://github.com/你的用户名/JHS-enhance/releases/latest/download/jhs-enhance.user.js
```
推送代码后,GitHub Actions 自动构建并将脚本上传到 Release,无需额外配置。
### 工作原理
本项目使用 GitHub Actions 在推送代码时自动构建并发布到 [GitHub Releases](https://github.com/fireinrain/JHS-enhance/releases)。
```
git push → GitHub Actions 自动构建 → 发布 GitHub Release
↓
https://github.com/用户名/JHS-enhance/releases/latest/download/jhs-enhance.user.js
https://github.com/fireinrain/JHS-enhance/releases/latest/download/jhs-enhance.user.js
↓
用户点击安装链接 → Tampermonkey 弹出安装提示
```
> **为什么不用 GitHub Pages?** Release 方案更简单,不需要配置 Pages,不需要额外权限,`/releases/latest/download/` 是固定链接,每次发版自动指向最新脚本。
---
## 功能概览
- **列表页增强**:作品状态标签(屏蔽/收藏/已下载/已观看)、一键打开待鉴定/已收藏作品、新作品检测、演员黑名单过滤
- **详情页增强**:磁力链接高亮、预览视频(DMM 多画质)、标题翻译、字幕搜索(SubTitleCat / 迅雷)、演员信息、相关清单
- **列表页增强**:作品状态标签(屏蔽/收藏/已下载/已观看)、一键筛选已收藏/新作品、演员黑名单过滤、标题关键词过滤
- **详情页增强**:磁力链接高亮匹配、DMM 多画质预览视频、标题翻译(Google 翻译)、字幕搜索(整合迅雷 + SubTitleCat,支持 115
网盘直传与多目录选择)、演员信息展示、相关清单、评论查看
- **多源预览图**:javfree / projectjav / javstore 三源预览图,支持来源切换、下载、新窗口打开,带 sessionStorage 缓存
- **115 网盘集成**:目录匹配与预加载、多目录选择上传、字幕一键直传
- **数据管理**:IndexedDB 本地存储、云盘备份/恢复(阿里云盘 / 115 网盘 / WebDAV)、跨 Tab 数据同步
- **自动化任务**:自动翻页、新作品检测、收藏演员同步、黑名单更新
- **UI 优化**:分类折叠、热榜 Top250、免 VIP 查看热播、以图识图、截图功能
- **UI 优化**:分类折叠、热榜 Top250、免 VIP 查看热播、以图识图、FC2 支持
- **多站点支持**:JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers / SubTitleCat
---
@@ -82,11 +71,11 @@ JHS-enhance/
│ ├── api/ # API 层
│ │ ├── javdb.js # JavDB API 封装(签名、搜索、评论等)
│ │ └── dmm.js # DMM 预览视频获取(多画质)
│ └── plugins/ # 插件层(38 个功能插件)
│ └── plugins/ # 插件层(40+ 个功能插件)
│ ├── list-page.js # 列表页过滤与状态标签
│ ├── list-page-button.js # 列表页功能按钮(待鉴定/已收藏/新作品/黑名单)
│ ├── detail-page.js # 详情页数据加载
│ ├── detail-page-button.js# 详情页操作按钮
│ ├── detail-page-button.js# 详情页操作按钮(含 115 目录预加载)
│ ├── setting.js # 设置面板(Tabulator 表格)
│ ├── translate.js # 标题翻译(Google 翻译 API)
│ ├── preview-video.js # DMM 预览视频
@@ -115,11 +104,13 @@ JHS-enhance/
│ ├── bus-img.js # JavBus 图片处理
│ ├── bus-preview-video.js # JavBus 预览视频
│ ├── image-recognition.js # 以图识图
│ ├── screenshot.js # 截图功能
│ ├── screenshot.js # 多源预览图(javfree/projectjav/javstore)
│ ├── subtitles.js # 字幕搜索(整合迅雷+SubTitleCat)+ 115 直传
│ ├── req115.js # 115 网盘 API 封装(Req + Drive115 + Req115)
│ ├── magnet-hub.js # MagnetHub 磁力搜索
│ ├── other-site.js # 第三方站点链接
│ ├── jav-trailers.js # JavTrailers 站点
│ ├── subtitle-cat.js # SubTitleCat 字幕搜索
│ ├── subtitle-cat.js # SubTitleCat 字幕搜索(已整合到 subtitles.js,保留文件)
│ ├── filter-title-keyword.js # 标题关键词过滤
│ └── want-watched.js # 想看/已看视频列表
├── vite.config.mjs # Vite + vite-plugin-monkey 配置
+2698 -354
View File
File diff suppressed because it is too large Load Diff
+1
View File
@@ -21,6 +21,7 @@ class BasePlugin {
__publicField(this, "videoSvg", '<svg t="1749003664455" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1952" width="200" height="200"><path d="M825.6 153.6H198.4C124.5 153.6 64 214.1 64 288v448c0 73.9 60.5 134.4 134.4 134.4h627.2c73.9 0 134.4-60.5 134.4-134.4V288c0-73.9-60.5-134.4-134.4-134.4z m-138.2 44.8l112 112H706l-112-112h93.4z m-156.8 0l112 112H526.7l-112-112h115.9z m-179.2 0l112 112H347.5l-112-112h115.9zM108.8 288c0-41.4 28.4-76.1 66.7-86.3l108.7 108.7H108.8V288z m806.4 448c0 49.4-40.2 89.6-89.6 89.6H198.4c-49.4 0-89.6-40.2-89.6-89.6V355.2h806.4V736z m0-425.6h-52.5l-112-112h74.9c49.4 0 89.6 40.2 89.6 89.6v22.4z" p-id="1953"></path><path d="M454 687.2l149.3-77.6c27.5-13.8 27.5-53 0-66.8L468 472.2c-31.2-15.6-68 7.1-68 42v139.6c0 27.8 29.2 45.8 54 33.4zM444.8 512l134.4 67.2-134.4 67.2V512z" p-id="1954"></path></svg>');
__publicField(this, "screenSvg", '<svg t="1750691468062" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2693" width="200" height="200"><path d="M288 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64v-576a64 64 0 0 0-64-64h-448m0-64h448a128 128 0 0 1 128 128v576a128 128 0 0 1-128 128h-448a128 128 0 0 1-128-128v-576a128 128 0 0 1 128-128z" fill="#4078FD" p-id="2694"></path><path d="M416 352m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#FE9C23" p-id="2695"></path><path d="M352 732.448a32 32 0 0 1-32-32v-160a32 32 0 0 1 44.224-29.568l130.112 53.632 153.952-169.984a32 32 0 0 1 55.712 21.472v284.448a32 32 0 0 1-32 32z m0-32h320z" fill="#4078FD" opacity=".2" p-id="2696"></path><path d="M672 416l-169.088 186.656-150.912-62.208v160h320V416m0-32a32 32 0 0 1 32 32v284.448a32 32 0 0 1-32 32h-320a32 32 0 0 1-32-32v-160a32 32 0 0 1 44.192-29.6l130.112 53.632 153.984-169.984a32 32 0 0 1 23.712-10.496z" fill="#4078FD" p-id="2697"></path></svg>');
__publicField(this, "recoveryVideoSvg", '<svg t="1749003779161" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8204" width="200" height="200"><path d="M938.666667 553.92V768c0 64.8-52.533333 117.333333-117.333334 117.333333H202.666667c-64.8 0-117.333333-52.533333-117.333334-117.333333V256c0-64.8 52.533333-117.333333 117.333334-117.333333h618.666666c64.8 0 117.333333 52.533333 117.333334 117.333333v297.92z m-64-74.624V256a53.333333 53.333333 0 0 0-53.333334-53.333333H202.666667a53.333333 53.333333 0 0 0-53.333334 53.333333v344.48A290.090667 290.090667 0 0 1 192 597.333333a286.88 286.88 0 0 1 183.296 65.845334C427.029333 528.384 556.906667 437.333333 704 437.333333c65.706667 0 126.997333 16.778667 170.666667 41.962667z m0 82.24c-5.333333-8.32-21.130667-21.653333-43.648-32.917333C796.768 511.488 753.045333 501.333333 704 501.333333c-121.770667 0-229.130667 76.266667-270.432 188.693334-2.730667 7.445333-7.402667 20.32-13.994667 38.581333-7.68 21.301333-34.453333 28.106667-51.370666 13.056-16.437333-14.634667-28.554667-25.066667-36.138667-31.146667A222.890667 222.890667 0 0 0 192 661.333333c-14.464 0-28.725333 1.365333-42.666667 4.053334V768a53.333333 53.333333 0 0 0 53.333334 53.333333h618.666666a53.333333 53.333333 0 0 0 53.333334-53.333333V561.525333zM320 480a96 96 0 1 1 0-192 96 96 0 0 1 0 192z m0-64a32 32 0 1 0 0-64 32 32 0 0 0 0 64z" fill="#000000" p-id="8205"></path></svg>');
__publicField(this, "markDeleteSvg", '<svg class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 832c-212.1 0-384-171.9-384-384s171.9-384 384-384 384 171.9 384 384-171.9 384-384 384z"></path><path d="M717.3 338.7L685.3 306.7 512 480 338.7 306.7 306.7 338.7 480 512 306.7 685.3 338.7 717.3 512 544 685.3 717.3 717.3 685.3 544 512z"></path></svg>');
}
getName() {
throw new Error(`${this.constructor.name} 未显示getName()`);
+380
View File
@@ -451,6 +451,385 @@ 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);
}
</style>
`;
const subtitleModalCss = `
<style>
.pdb-sub-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999999;
display: flex;
align-items: center;
justify-content: center;
}
.pdb-sub-modal {
background: #fff;
border-radius: 10px;
width: 85vw;
max-width: 1100px;
height: 85vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.2);
}
.pdb-sub-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
flex-shrink: 0;
}
.pdb-sub-search-wrap {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.pdb-sub-title {
font-weight: 600;
font-size: 14px;
color: #374151;
white-space: nowrap;
}
.pdb-sub-input {
flex: 1;
max-width: 400px;
padding: 8px 14px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.pdb-sub-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.pdb-sub-btn {
padding: 8px 18px;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
white-space: nowrap;
}
.pdb-sub-btn:hover {
background: #2563eb;
}
.pdb-sub-115-wrap {
display: flex;
align-items: center;
gap: 8px;
margin-left: 16px;
flex-shrink: 1;
min-width: 0;
max-width: 360px;
}
.pdb-sub-checkbox-group {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
min-width: 0;
}
.pdb-sub-checkbox-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #374151;
cursor: pointer;
white-space: nowrap;
}
.pdb-sub-checkbox {
margin: 0;
cursor: pointer;
}
.pdb-sub-close {
font-size: 26px;
color: #9ca3af;
cursor: pointer;
padding: 0 8px;
line-height: 1;
transition: color 0.2s;
}
.pdb-sub-close:hover {
color: #ef4444;
}
.pdb-sub-body {
display: flex;
flex: 1;
overflow: hidden;
}
.pdb-sub-content {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.pdb-sub-preview-wrap {
width: 45%;
display: flex;
flex-direction: column;
border-left: 1px solid #e5e7eb;
background: #1e1e1e;
}
.pdb-sub-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid #333;
color: #d1d5db;
font-size: 13px;
flex-shrink: 0;
}
.pdb-sub-preview-status {
font-size: 12px;
color: #6b7280;
}
.pdb-sub-textarea {
flex: 1;
background: #1e1e1e;
color: #2ecc71;
border: none;
padding: 14px;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
line-height: 1.5;
resize: none;
outline: none;
overflow-y: auto;
}
.pdb-sub-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.pdb-sub-table thead {
position: sticky;
top: 0;
z-index: 1;
}
.pdb-sub-th {
padding: 10px 12px;
background: #f3f4f6;
color: #374151;
font-weight: 600;
text-align: left;
border-bottom: 2px solid #e5e7eb;
white-space: nowrap;
}
.pdb-sub-tr {
transition: background 0.15s;
}
.pdb-sub-tr:hover {
background: #f9fafb;
}
.pdb-sub-tr.is-previewing {
background: #eff6ff;
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
.pdb-sub-td,
.pdb-sub-td-lang,
.pdb-sub-td-ext,
.pdb-sub-td-provider {
padding: 9px 12px;
border-bottom: 1px solid #f3f4f6;
vertical-align: middle;
}
.pdb-sub-td {
word-break: break-all;
}
.pdb-sub-td-lang,
.pdb-sub-td-ext {
text-align: center;
white-space: nowrap;
}
.pdb-sub-td-actions {
padding: 9px 12px;
border-bottom: 1px solid #f3f4f6;
text-align: center;
white-space: nowrap;
}
.pdb-sub-provider {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.pdb-sub-provider--xunlei {
background: #dbeafe;
color: #1d4ed8;
}
.pdb-sub-provider--subtitlecat {
background: #fef3c7;
color: #92400e;
}
.pdb-sub-action-btn {
padding: 4px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
margin: 0 2px;
transition: opacity 0.2s;
}
.pdb-sub-btn-preview {
background: #e0f2fe;
color: #0369a1;
}
.pdb-sub-btn-preview:hover {
background: #bae6fd;
}
.pdb-sub-btn-download {
background: #dcfce7;
color: #166534;
}
.pdb-sub-btn-download:hover {
background: #bbf7d0;
}
.pdb-sub-btn-upload {
background: #ede9fe;
color: #5b21b6;
}
.pdb-sub-btn-upload:hover {
background: #ddd6fe;
}
.pdb-sub-msg {
text-align: center;
padding: 40px 20px;
color: #6b7280;
font-size: 14px;
}
</style>
`;
@@ -470,6 +849,7 @@ insertStyle(
);
insertStyle(mainCss);
insertStyle(subtitleModalCss);
export {
__defProp,
-2
View File
@@ -2,7 +2,6 @@ import $ from 'jquery';
import localforage from 'localforage';
import Toastify from 'toastify-js';
import md5 from 'blueimp-md5';
import { TabulatorFull as Tabulator } from 'tabulator-tables';
import Viewer from 'viewerjs';
import QRCode from 'qrcodejs';
import { currentHref, isJavDb, isJavBus, isSearchPage, Status_RUNNING, Status_SUCCESS, Status_FAIL, Status_LOADING, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH, NO, YES } from './constants.js';
@@ -11,7 +10,6 @@ window.$ = window.jQuery = $;
window.localforage = localforage;
window.Toastify = Toastify;
window.md5 = md5;
window.Tabulator = Tabulator;
window.Viewer = Viewer;
window.QRCode = QRCode;
+175
View File
@@ -0,0 +1,175 @@
/*
* https://github.com/Tampermonkey/tampermonkey/issues/2215
*
* This script provides a workaround for a Chrome MV3 issue (https://github.com/w3c/webextensions/issues/694)
* where extensions can't set/delete headers that are preserved over redirects.
*
* By setting `redirect: 'manual'` and following redirects manually, this script ensures request redirects work
* as intended and requests to different URLs are made in parallel (again).
*
* Userscript authors should include this as a `@require` when they need to make parallel requests with GM_xmlhttpRequest,
* especially if requests might take a long time to complete.
*
* Including this script will modify the behavior of GM_xmlhttpRequest and GM.xmlHttpRequest in Tampermonkey only.
*
* Usage:
*
* Add this to the metadata block of your userscript:
*
* // @grant GM_xmlhttpRequest
* // @require https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js
*
**/
/* global GM_info, GM_xmlhttpRequest, GM */
const HAS_GM = typeof GM !== 'undefined';
const NEW_GM = ((scope, GM) => {
// Check if running in Tampermonkey and if version supports redirect control
if (typeof GM_info === 'undefined' || GM_info.scriptHandler !== "Tampermonkey" || compareVersions(GM_info.version, "5.3.2") < 0) return;
// Backup original functions
const GM_xmlhttpRequestOrig = GM_xmlhttpRequest;
const GM_xmlHttpRequestOrig = GM.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;
}
return 0;
}
// Wrapper for GM_xmlhttpRequest
function GM_xmlhttpRequestWrapper(odetails) {
// If redirect is manually set, simply pass odetails to the original function
if (odetails.redirect !== undefined) {
return GM_xmlhttpRequestOrig(odetails);
}
// Warn if onprogress is used with settings incompatible with fetch mode used in background
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;
// Set redirect to manual and handle redirects
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);
// Follow redirect manually
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);
}
// Wrapper for GM.xmlHttpRequest
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) {
// See comment above
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;
}
// Export wrappers
GM_xmlhttpRequest = GM_xmlhttpRequestWrapper;
scope.GM_xmlhttpRequestOrig = GM_xmlhttpRequestOrig;
const gopd = Object.getOwnPropertyDescriptor(GM, 'xmlHttpRequest');
if (gopd && gopd.configurable === false) {
return {
__proto__: GM,
xmlHttpRequest: GM_xmlHttpRequestWrapper,
xmlHttpRequestOrig: GM_xmlHttpRequestOrig
};
} else {
GM.xmlHttpRequest = GM_xmlHttpRequestWrapper;
GM.xmlHttpRequestOrig = GM_xmlHttpRequestOrig;
}
})(globalThis, HAS_GM ? GM : {});
if (HAS_GM && NEW_GM) GM = NEW_GM;
+4 -1
View File
@@ -1,4 +1,5 @@
import './core/globals.js';
import './lib/gm-xhr-parallel.js';
import { __defProp, __typeError, __publicField, __privateMethod, currentHref, isJavDb, isJavBus, isSearchPage, Status_RUNNING, Status_SUCCESS, Status_FAIL, Status_LOADING, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH, NO, YES, qualityOptions, detailPageCss$1, javBusStyle, detailPageCss, javdbStyle, mainCss, insertStyle } from './core/constants.js';
import { StorageManager } from './core/storage.js';
import { Utils } from './core/utils.js';
@@ -16,6 +17,8 @@ import { DetailPagePlugin } from './plugins/detail-page.js';
import { PreviewVideoPlugin } from './plugins/preview-video.js';
import { JavTrailersPlugin } from './plugins/jav-trailers.js';
import { SubTitleCatPlugin } from './plugins/subtitle-cat.js';
import {JavPackSubtitle} from './plugins/subtitles.js';
import {Req115} from './plugins/req115.js';
import { Fc2Plugin } from './plugins/fc2.js';
import { HighlightMagnetPlugin } from './plugins/highlight-magnet.js';
import { FoldCategoryPlugin } from './plugins/fold-category.js';
@@ -846,7 +849,7 @@ utils.importResource("https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toasti
utils.importResource("https://cdn.jsdelivr.net/npm/viewerjs@1.11.1/dist/viewer.min.css");
utils.importResource("https://cdn.jsdelivr.net/npm/tabulator-tables@6.3.1/dist/css/tabulator_semanticui.min.css");
utils.importResource("https://cdn.jsdelivr.net/npm/tabulator-tables@6.5.2/dist/css/tabulator_semanticui.min.css");
const pluginManager = function() {
const pluginManager2 = new PluginManager;
File diff suppressed because one or more lines are too long
+12 -4
View File
@@ -22,7 +22,8 @@ class DetailPageButtonPlugin extends BasePlugin {
window.isDetailPage && this.createMenuBtn();
}
async createMenuBtn() {
const pageInfo = this.getPageInfo(), carNum2 = pageInfo.carNum, buttonsHtml = '\n <div style="margin: 10px auto; display: flex; justify-content: space-between; align-items: center; flex-wrap:wrap;gap: 20px;">\n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="filterBtn" class="menu-btn" style="width: 120px; background-color:#de3333; color: white; text-align: center; padding: 8px 0;">\n <span>🚫 屏蔽</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="width: 120px; background-color:#25b1dc; color: white; text-align: center; padding: 8px 0;">\n <span>⭐ 收藏</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="width: 120px; background-color:#7bc73b; color: white; text-align: center; padding: 8px 0;">\n <span>📥️ 已下载</span>\n </a>\n <a id="hasWatchBtn" class="menu-btn" style="width: 120px; background-color:#d7a80c; color: white; text-align: center; padding: 8px 0;">\n <span>🔍 已观看</span>\n </a>\n </div>\n \n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="enable-magnets-filter" class="menu-btn" style="width: 140px; background-color: #c2bd4c; color: white; text-align: center; padding: 8px 0;">\n <span id="magnets-span">关闭磁力过滤</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="search-subtitle-btn" class="menu-btn" style="width: 160px; background: linear-gradient(to bottom, #8d5656, rgb(196,159,91)); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (SubTitleCat)</span>\n </a>\n </div>\n </div>\n ';
const pageInfo = this.getPageInfo(), carNum2 = pageInfo.carNum,
buttonsHtml = '\n <div style="margin: 10px auto; display: flex; justify-content: space-between; align-items: center; flex-wrap:wrap;gap: 20px;">\n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="filterBtn" class="menu-btn" style="width: 120px; background-color:#de3333; color: white; text-align: center; padding: 8px 0;">\n <span>🚫 屏蔽</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="width: 120px; background-color:#25b1dc; color: white; text-align: center; padding: 8px 0;">\n <span>⭐ 收藏</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="width: 120px; background-color:#7bc73b; color: white; text-align: center; padding: 8px 0;">\n <span>📥️ 已下载</span>\n </a>\n <a id="hasWatchBtn" class="menu-btn" style="width: 120px; background-color:#d7a80c; color: white; text-align: center; padding: 8px 0;">\n <span>🔍 已观看</span>\n </a>\n </div>\n \n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="enable-magnets-filter" class="menu-btn" style="width: 140px; background-color: #c2bd4c; color: white; text-align: center; padding: 8px 0;">\n <span id="magnets-span">关闭磁力过滤</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n <a id="subtitleSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕搜索</span>\n </a>\n <!--\n <a id="xunLeiSubtitleBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="search-subtitle-btn" class="menu-btn" style="width: 160px; background: linear-gradient(to bottom, #8d5656, rgb(196,159,91)); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (SubTitleCat)</span>\n </a>\n -->\n </div>\n </div>\n ';
isJavDb && $(".tabs").after(buttonsHtml);
isJavBus && $("#mag-submit-show").before(buttonsHtml);
$("#favoriteBtn").on("click", (() => this.favoriteOne()));
@@ -57,8 +58,12 @@ class DetailPageButtonPlugin extends BasePlugin {
storageManager.saveSettingItem("enableMagnetsFilter", YES);
}
}));
$("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
$("#xunLeiSubtitleBtn").on("click", (() => this.searchXunLeiSubtitle(carNum2)));
$("#subtitleSearchBtn").on("click", (() => {
window.JavPackSubtitle.openSearchModal({details: {code: carNum2}});
}));
window.JavPackSubtitle.preload115Matches(carNum2);
// $("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
// $("#xunLeiSubtitleBtn").on("click", (() => this.searchXunLeiSubtitle(carNum2)));
this.showStatus(carNum2).then();
}
async showStatus(carNum2) {
@@ -124,6 +129,8 @@ class DetailPageButtonPlugin extends BasePlugin {
window.refresh();
utils.closePage();
}
/*
searchXunLeiSubtitle(carNum2) {
let loadObj = loading();
gmHttp.get(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?gcid=&cid=&name=${carNum2}`).then((res => {
@@ -205,6 +212,7 @@ class DetailPageButtonPlugin extends BasePlugin {
loadObj.close();
}));
}
*/
async filterOne(event, noAlert) {
event && event.preventDefault();
let pageInfo = this.getPageInfo();
@@ -333,4 +341,4 @@ class DetailPageButtonPlugin extends BasePlugin {
}
export { DetailPageButtonPlugin };
export { DetailPageButtonPlugin };
+9 -5
View File
@@ -171,7 +171,7 @@ class Fc2By123AvPlugin extends BasePlugin {
async open123AvFc2Dialog(carNum2, href) {
let otherHtml = "";
await storageManager.getSetting("enableLoadOtherSite", YES) === YES && (otherHtml = '<div class="movie-panel-info fc2-movie-panel-info" style="margin-top:20px"><strong>第三方站点: </strong></div>');
let pageHtml = `\n <div class="movie-detail-container">\n \x3c!-- <div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>\n <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n ${otherHtml}\n \n <div style="margin: 10px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\n \n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links">\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!-- </div>--\x3e\n </div>\n `;
let pageHtml = `\n <div class="movie-detail-container">\n \x3c!-- <div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>\n <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n ${otherHtml}\n \n <div style="margin: 10px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\n \n <a id="subtitleSearchBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕搜索</span>\n </a>\n <!--\n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n -->\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links">\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!-- </div>--\x3e\n </div>\n `;
layer.open({
type: 1,
title: carNum2,
@@ -235,8 +235,12 @@ class Fc2By123AvPlugin extends BasePlugin {
window.refresh();
layer.closeAll();
}));
$("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
$("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2)));
$("#subtitleSearchBtn").on("click", (() => {
window.JavPackSubtitle.openSearchModal({details: {code: carNum2}});
}));
window.JavPackSubtitle.preload115Matches(carNum2);
// $("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
// $("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2)));
let tempCarNum = carNum2.replace("FC2-", "");
this.getBean("OtherSitePlugin").loadOtherSite(tempCarNum, carNum2).then();
}
@@ -265,7 +269,7 @@ class Fc2By123AvPlugin extends BasePlugin {
$(".screen-container").on("click", (event => {
event.stopPropagation();
event.preventDefault();
showImageViewer(event.currentTarget);
showPreviewOverlay(imgUrl, carNum2);
}));
}
}));
@@ -351,4 +355,4 @@ class Fc2By123AvPlugin extends BasePlugin {
}
export { Fc2By123AvPlugin };
export { Fc2By123AvPlugin };
+8 -4
View File
@@ -33,7 +33,7 @@ class Fc2Plugin extends BasePlugin {
layer.open({
type: 1,
title: carNum2,
content: '\n <div class="movie-detail-container">\n \x3c!--<div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>--\x3e\n \x3c!-- <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n <div class="movie-panel-info fc2-movie-panel-info" style="margin-top:20px"><strong>第三方资源: </strong></div>\n \n <div style="margin: 30px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\n \n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn fr-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links" style="margin: 0 0.75rem">\n <div class="search-loading">加载中...</div>\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!--</div>--\x3e\n </div>\n ',
content: '\n <div class="movie-detail-container">\n \x3c!--<div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>--\x3e\n \x3c!-- <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n <div class="movie-panel-info fc2-movie-panel-info" style="margin-top:20px"><strong>第三方资源: </strong></div>\n \n <div style="margin: 30px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\n \n <a id="subtitleSearchBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕搜索</span>\n </a>\n <!--\n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n -->\n <a id="magnetSearchBtn" class="menu-btn fr-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links" style="margin: 0 0.75rem">\n <div class="search-loading">加载中...</div>\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!--</div>--\x3e\n </div>\n ',
area: utils.getResponsiveArea([ "70%", "90%" ]),
skin: "movie-detail-layer",
scrollbar: !1,
@@ -90,8 +90,12 @@ class Fc2Plugin extends BasePlugin {
window.refresh();
layer.closeAll();
}));
$("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
$("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2)));
$("#subtitleSearchBtn").on("click", (() => {
window.JavPackSubtitle.openSearchModal({details: {code: carNum2}});
}));
window.JavPackSubtitle.preload115Matches(carNum2);
// $("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
// $("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2)));
$("#magnetSearchBtn").on("click", (() => {
let magnetHub = this.getBean("MagnetHubPlugin").createMagnetHub(carNum2);
layer.open({
@@ -147,7 +151,7 @@ class Fc2Plugin extends BasePlugin {
utils.loopDetector((() => $(".movie-gallery .image-list").length > 0), (async () => {
$(".movie-gallery .image-list").prepend(' <a class="tile-item screen-container" style="overflow:hidden;max-height: 150px;max-width:150px; text-align:center;"><div style="margin-top: 50px;color: #000;cursor: auto">正在加载缩略图</div></a> ');
const screenShotPlugin = this.getBean("ScreenShotPlugin"), imgUrl = await screenShotPlugin.getScreenshot(carNum2);
imgUrl && await screenShotPlugin.addImg("缩略图", imgUrl);
imgUrl && await screenShotPlugin.addImg("缩略图", imgUrl, carNum2);
}));
}
handleMagnets(movieId) {
+78 -1
View File
@@ -10,6 +10,7 @@ class ListPageButtonPlugin extends BasePlugin {
await this.createMenuBtn();
this.bindEvent();
await storageManager.getSetting("autoPage") === YES ? $("#sort-toggle-btn").hide() : this.sortItems().then();
this.initOneTimeSortBtn();
}
async createMenuBtn() {
const showWaitCheckBtn = await storageManager.getSetting("showWaitCheckBtn", YES), showWaitDownBtn = await storageManager.getSetting("showWaitDownBtn", YES);
@@ -38,7 +39,7 @@ class ListPageButtonPlugin extends BasePlugin {
const isFc2Page2 = currentHref.includes("advanced_search");
isFc2Page2 ? $el = $("h2.section-title") : otherCss = "flex-grow:1;";
const jhs_sortMethod = localStorage.getItem("jhs_sortMethod"), sortText = "当前排序方式: " + ("rateCount" === jhs_sortMethod ? "评价人数" : "date" === jhs_sortMethod ? "时间" : "默认");
$el.append(`\n <div style="display: flex;align-items: center; ${otherCss} ">\n <a id="waitCheckBtn" class="menu-btn main-tab-btn" data-tip="打开未鉴定的列表项, 并自动播放视频" style="background-color:#56c938 !important; ${showWaitCheckBtn === NO ? "display: none" : ""}"><span>打开待鉴定</span></a>\n <a id="waitDownBtn" class="menu-btn main-tab-btn" style="background-color:#2caac0 !important; ${showWaitDownBtn === NO ? "display: none" : ""}"><span>打开已收藏</span></a>\n ${isStarPage ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${addBlacklistBtnColor} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${addBlacklistBtnText}</span></a>\n <a id="filterAllVideo" class="menu-btn main-tab-btn" style="background-color:#e8ab39 !important;margin-right: 30px!important;" data-tip="一键屏蔽已选分类的视频列表至鉴定记录中"><span>一键屏蔽所有作品</span></a>\n ` : ""}\n ${currentHref.includes("/tags") ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${addBlacklistBtnColor} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${addBlacklistBtnText}</span></a>\n ` : ""}\n </div>\n <div style="display: flex;align-items: center;">\n <a id="newVideoBtn" class="menu-btn main-tab-btn" style="background-color:#2c6cc0 !important;"><span>新作品检测 (<span id="newVideoCount">0</span>)</span></a>\n <a id="blacklistBtn" class="menu-btn main-tab-btn" style="background-color:#34393f !important;"><span>演员黑名单</span></a>\n ${isSearchPage || isFc2Page2 ? "" : `<a id="sort-toggle-btn" class="menu-btn main-tab-btn" style="background-color:#8783ab !important;"> ${sortText} </a>`}\n </div>\n `);
$el.append(`\n <div style="display: flex;align-items: center; ${otherCss} ">\n <a id="waitCheckBtn" class="menu-btn main-tab-btn" data-tip="打开未鉴定的列表项, 并自动播放视频" style="background-color:#56c938 !important; ${showWaitCheckBtn === NO ? "display: none" : ""}"><span>打开待鉴定</span></a>\n <a id="waitDownBtn" class="menu-btn main-tab-btn" style="background-color:#2caac0 !important; ${showWaitDownBtn === NO ? "display: none" : ""}"><span>打开已收藏</span></a>\n ${isStarPage ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${addBlacklistBtnColor} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${addBlacklistBtnText}</span></a>\n <a id="filterAllVideo" class="menu-btn main-tab-btn" style="background-color:#e8ab39 !important;margin-right: 30px!important;" data-tip="一键屏蔽已选分类的视频列表至鉴定记录中"><span>一键屏蔽所有作品</span></a>\n ` : ""}\n ${currentHref.includes("/tags") ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${addBlacklistBtnColor} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${addBlacklistBtnText}</span></a>\n ` : ""}\n </div>\n <div style="display: flex;align-items: center;">\n <a id="one-time-sort-btn" class="menu-btn main-tab-btn" style="background-color:#8783ab !important;">一次性排序: 默认</a>\n <a id="newVideoBtn" class="menu-btn main-tab-btn" style="background-color:#2c6cc0 !important;"><span>新作品检测 (<span id="newVideoCount">0</span>)</span></a>\n <a id="blacklistBtn" class="menu-btn main-tab-btn" style="background-color:#34393f !important;"><span>演员黑名单</span></a>\n ${isSearchPage || isFc2Page2 ? "" : `<a id="sort-toggle-btn" class="menu-btn main-tab-btn" style="background-color:#8783ab !important;"> ${sortText} </a>`}\n </div>\n `);
}
if (isJavBus) {
const isStarPage = currentHref.includes("/star/");
@@ -90,6 +91,22 @@ class ListPageButtonPlugin extends BasePlugin {
localStorage.setItem("jhs_sortMethod", newMethod);
this.sortItems().then();
}));
$("#one-time-sort-btn").on("click", (event => {
const currentMethod = localStorage.getItem("jhs_oneTimeSortMethod") || "default";
const sortCycle = ["default", "rateCount", "totalScore", "date"];
let nextIndex = sortCycle.indexOf(currentMethod) + 1;
if (nextIndex >= sortCycle.length || nextIndex < 0) nextIndex = 0;
const newMethod = sortCycle[nextIndex];
const methodText = {
default: "默认",
rateCount: "评价人数",
totalScore: "总分",
date: "时间"
}[newMethod];
$("#one-time-sort-btn").text(`一次性排序: ${methodText}`);
localStorage.setItem("jhs_oneTimeSortMethod", newMethod);
this.oneTimeSortItems().then();
}));
const blacklistPlugin = this.getBean("BlacklistPlugin");
$("#addBlacklistBtn").on("click", (async event => {
await blacklistPlugin.addBlacklist(event);
@@ -174,6 +191,66 @@ class ListPageButtonPlugin extends BasePlugin {
$container.empty().append(items);
}
}
async initOneTimeSortBtn() {
const autoPage = await storageManager.getSetting("autoPage");
if (autoPage !== YES) {
$("#one-time-sort-btn").hide();
return;
}
const oneTimeMethod = localStorage.getItem("jhs_oneTimeSortMethod") || "default";
const methodText = {
default: "默认",
rateCount: "评价人数",
totalScore: "总分",
date: "时间"
}[oneTimeMethod];
$("#one-time-sort-btn").text(`一次性排序: ${methodText}`).show();
}
async oneTimeSortItems() {
if (currentHref.includes("handle") || currentHref.includes("advanced_search")) return;
if (isSearchPage) return;
const method = localStorage.getItem("jhs_oneTimeSortMethod");
if (!method) return;
$(".movie-list .item").each((function (index) {
$(this).attr("data-original-index") || $(this).attr("data-original-index", index);
}));
const $container = $(".movie-list"), $items = $(".item", $container);
if ("default" === method) {
$items.sort((function (a, b) {
return $(a).data("original-index") - $(b).data("original-index");
})).appendTo($container);
} else {
const items = $items.get();
items.sort((function (a, b) {
if ("rateCount" === method) {
const getScore = el => {
const match = $(el).find(".score .value").text().match(/由(\d+)人/);
return match ? parseFloat(match[1]) : 0;
};
return getScore(b) - getScore(a);
} else if ("totalScore" === method) {
const getTotalScore = el => {
const text = $(el).find(".score .value").text();
const scoreMatch = text.match(/([\d.]+)分/);
const countMatch = text.match(/由(\d+)人/);
const score = scoreMatch ? parseFloat(scoreMatch[1]) : 0;
const count = countMatch ? parseFloat(countMatch[1]) : 0;
return score * count;
};
return getTotalScore(b) - getTotalScore(a);
} else {
const getDate = el => {
const dateStr = $(el).find(".meta").text().trim();
return new Date(dateStr);
};
return getDate(b) - getDate(a);
}
}));
$container.empty().append(items);
}
}
async openWaitCheck(event) {
const maxCount = await storageManager.getSetting("waitCheckCount", 5);
let count = 0;
+1 -1
View File
@@ -56,9 +56,9 @@ class ListPagePlugin extends BasePlugin {
this.replaceHdImg();
this.addJumpPageControl();
this.fixBusTitleBox();
this.getBean("CoverButtonPlugin").addSvgBtn().then();
await this.doFilter();
await this.getBean("ListPageButtonPlugin").sortItems();
this.getBean("CoverButtonPlugin").addSvgBtn().then();
$(this.getSelector().itemSelector + " a").attr("target", "_blank");
this.getBean("AutoPagePlugin").checkLoad();
} finally {
+528
View File
@@ -0,0 +1,528 @@
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
class Req {
static DEFAULT_GET_RESPONSE_TYPE = "document";
static DEFAULT_POST_RESPONSE_TYPE = "json";
static DEFAULT_TIMEOUT = 30000;
static DEFAULT_METHOD = "GET";
static _isRawBody = (v) => v instanceof FormData || v instanceof Blob || v instanceof ArrayBuffer || v instanceof URLSearchParams;
static _isPlainObject = (v) => Object.prototype.toString.call(v) === "[object Object]";
static _serializeToFormData(formData, data, parentKey = "") {
Object.entries(data).forEach(([key, val]) => {
const fullKey = parentKey ? `${parentKey}[${key}]` : key;
if (val === undefined) 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 ||= 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 ??= 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.url.split(/[?#]/)[0].endsWith(".json") ? "json" : this.DEFAULT_GET_RESPONSE_TYPE;
}
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(new DOMException("Aborted", "AbortError"));
const {url: targetUrl} = options;
let xhrHandler;
const handleAbort = () => xhrHandler?.abort();
const finalize = (settleFn, ...args) => {
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: undefined,
});
signal?.addEventListener("abort", handleAbort, {once: true});
});
}
static async tasks(initial, steps, signal) {
let ctx = initial;
for (const step of steps) {
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
if (typeof ctx === "string" || ctx?.url) ctx = await this.request(typeof ctx === "string" ? {
url: ctx,
signal
} : {...ctx, signal});
ctx = await step(ctx);
}
return ctx;
}
}
class Drive115 extends Req {
static DEFAULT_GET_RESPONSE_TYPE = "json";
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: name,
object: key,
policy,
accessid: OSSAccessKeyId,
callback,
signature,
file,
}) {
return this.post({
url,
data: {
name,
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},
});
}
}
export 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) {
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 (res?.path?.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((folder) => folder.n === route);
if (!folder) folder = await this.filesAdd(route, cid);
cid = folder?.cid;
if (!cid) break;
}
}
const month = 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, 1000);
});
};
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((task) => task.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((task) => task.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 name = item.n.toUpperCase();
return codes.some((code) => name.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) {
const {data} = await this.labelList();
if (!data?.list?.length) return;
const {list} = data;
const labels = [];
tags.forEach((tag) => {
const item = list.find((item) => item.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].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: 60000, responseType: "blob"});
if (!file) return;
const res = await this.sampleInitUpload({cid, filename, filesize: file.size});
if (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?.file_id) this.filesEdit(file_id, data.file_id);
} catch (err) {
console.warn("[Req115.handleCover]", err?.message);
}
}
res.msg = `${code} 离线任务成功`;
res.status = "success";
break;
}
return res;
}
static async queueMutation(name, fn) {
return fn();
}
}
window.Req115 = Req115;
+454 -115
View File
@@ -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 = '<div class="jhs-loading-spinner"><div class="spinner"></div><div>正在加载预览图...</div></div>';
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(' <a class="tile-item screen-container" style="overflow:hidden;max-height: 215px;text-align:center;"><div style="margin-top: 50px;color: #000;cursor: auto">正在加载缩略图</div></a> ');
isJavBus && $("#sample-waterfall .sample-box:first").after(' <a class="sample-box screen-container" style="overflow:hidden; height: 110px; text-align:center;"><div style="margin-top: 30px;color: #000;cursor: auto">正在加载缩略图</div></a> ');
isJavDb && $(".preview-images .tile-item").first().before(
'<a class="tile-item screen-container" style="overflow:hidden;max-height:215px;text-align:center;">' +
'<div style="margin-top:50px;color:#000;cursor:auto">正在加载缩略图</div></a>'
);
isJavBus && $("#sample-waterfall .sample-box:first").after(
'<a class="sample-box screen-container" style="overflow:hidden;height:110px;text-align:center;">' +
'<div style="margin-top:30px;color:#000;cursor:auto">正在加载缩略图</div></a>'
);
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(`<img src="${imgUrl}" alt="${title}" loading="lazy" style="width: 100%;">`);
isJavBus && $(".screen-container").html(`<div class="photo-frame"><img src="${imgUrl}" style="height: inherit;width: 100%;" title="${title}" alt="${title}"></div>`);
$(".screen-container").on("click", (event => {
event.stopPropagation();
event.preventDefault();
showImageViewer(event.currentTarget);
}));
}
addImg(title, imgUrl, code, source) {
if (!imgUrl) return;
isJavDb && $(".screen-container").html(
`<img src="${imgUrl}" alt="${title}" loading="lazy" style="width:100%;">`
);
isJavBus && $(".screen-container").html(
`<div class="photo-frame"><img src="${imgUrl}" style="height:inherit;width:100%;" title="${title}" alt="${title}"></div>`
);
$(".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(`<div style="${differentCss}; cursor:auto;color:#000;">获取缩略图失败</div><br/><a href='#' class='retry-link'>点击重试</a> 或 <a class="check-link" href='https://javstore.net/search/${carNum2}.html' target='_blank'>前往确认</a>`).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(
`<div style="${differentCss};cursor:auto;color:#000;">获取缩略图失败</div>` +
`<br/><a href='#' class='retry-link'>点击重试</a> 或 ` +
`<a class="check-link" href='https://javstore.net/search?q=${encodeURIComponent(carNum2)}' target='_blank'>前往确认</a>`
).off("click", ".retry-link").off("click", ".check-link")
.on("click", ".retry-link", async (e) => {
e.stopPropagation();
e.preventDefault();
$(".screen-container").html(`<div style="${differentCss};cursor:auto;color:#000;">正在重新加载...</div>`);
$(".screen-container").html(
`<div style="${differentCss};cursor:auto;color:#000;">正在重新加载...</div>`
);
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 };
export {ScreenShotPlugin, Thumbnail, showPreviewOverlay};
window.showPreviewOverlay = showPreviewOverlay;
+127 -4
View File
File diff suppressed because one or more lines are too long
+605
View File
@@ -0,0 +1,605 @@
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
/**
* @name JavPack Subtitle Library
* @description Xunlei and SubtitleCat subtitle search, preview, download, and 115 upload for JavDB.
*/
export class JavPackSubtitle {
static BTN_ID = "x-subtitle-search-btn";
static MODAL_ID = "x-subtitle-modal";
static previewCache = new Map();
static cached115Matches = [];
static current115Cids = [];
static last115Code = "";
static SUBTITLECAT_ORIGIN = "https://www.subtitlecat.com";
static SUBTITLECAT_CHINESE_LANGUAGES = new Map([
["zh-CN", "简中"],
["zh-TW", "繁中"],
]);
static escapeHtml(value = "") {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
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 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 buildSubtitleBaseName(details = {}) {
if (window.JavPackMatch115Console?.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 clearPreviewCache() {
this.previewCache.clear();
}
static async checkSubInCloud(req115, cid) {
if (!req115 || !cid) return false;
const res = await req115.filesAll(cid);
return (res?.data || []).some((file) => this.isSubtitleFile(file));
}
static getReq115() {
return window.Req115 || null;
}
static getTargetCid() {
return document.querySelector(".x-match-cont .zymatch-item [data-cid]")?.dataset.cid
|| document.querySelector(".x-match-cont .zymatch-item")?.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?.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?.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?.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?.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 = new Map();
(getSubtitleEntries?.() || []).forEach(({directory, files = []}) => {
const path = String(directory || "").trim();
if (!path) return;
if (!groups.has(path)) groups.set(path, new Set());
files.map((file) => String(file?.n || file?.name || file || "").trim())
.filter(Boolean)
.forEach((name) => groups.get(path).add(name));
});
btn.title = groups.size
? ["已有字幕文件:", ...[...groups].flatMap(([path, files]) => [
`目录:${path}`,
...[...files].map((name) => ` - ${name}`),
])].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 `
<div class="pdb-sub-modal">
<div class="pdb-sub-header">
<div class="pdb-sub-search-wrap">
<span class="pdb-sub-title">字幕检索:</span>
<input type="text" id="sub-search-input" value="${this.escapeHtml(defaultKw)}" class="pdb-sub-input" placeholder="输入检索词..." />
<button id="sub-search-btn" class="pdb-sub-btn">重新搜索</button>
</div>
<div class="pdb-sub-115-wrap" id="sub-115-wrap" style="display:none;">
<span class="pdb-sub-title">115目录:</span>
<div id="sub-115-select" class="pdb-sub-checkbox-group"></div>
</div>
<span class="pdb-sub-close" id="sub-close-btn">&times;</span>
</div>
<div class="pdb-sub-body">
<div class="pdb-sub-content"></div>
<div class="pdb-sub-preview-wrap">
<div class="pdb-sub-preview-header">
<span>字幕内容预览</span><span id="preview-status" class="pdb-sub-preview-status">暂无预览</span>
</div>
<textarea class="pdb-sub-textarea" readonly></textarea>
</div>
</div>
</div>
`;
}
static openSearchModal({details = {}, getTargetCid} = {}) {
const defaultKw = this.buildDefaultKeyword(details);
this.currentDetails = details;
document.getElementById(this.MODAL_ID)?.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 `<label class="pdb-sub-checkbox-label"><input type="checkbox" class="pdb-sub-checkbox" value="${m.cid}"${checked}><span title="${this.escapeHtml(m.name)}">${this.escapeHtml(displayName)}</span></label>`;
}).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 = '<div class="pdb-sub-msg">正在搜索字幕来源,请稍候...</div>';
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 = '<div class="pdb-sub-msg">未找到简中或繁中字幕,请尝试使用完整番号搜索</div>';
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 = '<div class="pdb-sub-msg">搜索失败,请稍后重试</div>';
}
};
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 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, 12000);
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 parseSubtitleCatSearch(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
return [...doc.querySelectorAll("table.sub-table tbody tr")].flatMap((row) => {
const link = row.querySelector('a[href*="subs/"]');
const name = link?.textContent?.trim();
if (!link || !name) return [];
return [{
name,
detailUrl: new URL(link.getAttribute("href"), this.SUBTITLECAT_ORIGIN).href,
}];
});
}
static parseSubtitleCatLanguages(html, entry) {
const doc = new DOMParser().parseFromString(html, "text/html");
return [...doc.querySelectorAll(".sub-single")].flatMap((node) => {
const languageCode = node.querySelector("img.flag")?.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 name = `${item.name || ""} ${item.extra_name || ""}`;
return /(zh[-_]?cn|zh[-_]?tw|chs|cht|chinese\s*(simplified|traditional)|简体|繁体|繁體|简中|繁中)/i.test(`${language} ${name}`);
}
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 name = (item.name || item.extra_name || "").toLowerCase();
const compactName = name.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 (name.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(name)) 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 = `
<table class="pdb-sub-table">
<thead>
<tr>
<th class="pdb-sub-th" style="width: 90px;">来源</th>
<th class="pdb-sub-th">原始字幕名称</th>
<th class="pdb-sub-th" style="width: 80px;">语言</th>
<th class="pdb-sub-th" style="width: 60px;">格式</th>
<th class="pdb-sub-th" style="width: 170px; text-align:center;">操作</th>
</tr>
</thead>
<tbody>
${dataList.map((item, index) => this.renderRow(item, index, highlightRegex)).join("")}
</tbody>
</table>
`;
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) {
const lang = item.languages?.[0] || "未知";
const name = 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(name).replace(highlightRegex, '<span style="color:#e74c3c; font-weight:bold;">$1</span>') : this.escapeHtml(name);
return `
<tr class="pdb-sub-tr">
<td class="pdb-sub-td-provider"><span class="pdb-sub-provider ${providerClass}">${this.escapeHtml(provider)}</span></td>
<td class="pdb-sub-td">${displayName}</td>
<td class="pdb-sub-td-lang">${this.escapeHtml(lang)}</td>
<td class="pdb-sub-td-ext">${this.escapeHtml(item.ext || "srt")}</td>
<td class="pdb-sub-td-actions">
<button class="sub-action-btn pdb-sub-action-btn pdb-sub-btn-preview" data-action="preview" data-idx="${index}">预览</button>
<button class="sub-action-btn pdb-sub-action-btn pdb-sub-btn-download" data-action="download" data-idx="${index}">下载</button>
<button class="sub-action-btn pdb-sub-action-btn pdb-sub-btn-upload" data-action="upload" data-idx="${index}">115直传</button>
</td>
</tr>
`;
}
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("<?xml") && text.includes("<Code>NoSuchKey</Code>")) 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?.() || 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((row) => {
row.classList.remove("is-previewing");
row.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?.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?.error_msg || uploadRes?.error || "115 服务器拒绝接收回调");
}
};
window.JavPackSubtitle = JavPackSubtitle;
+43
View File
@@ -145,6 +145,49 @@ class WangPan115TaskPlugin extends BasePlugin {
}
return fileId;
}
//TODO: 暂时没有删除离线任务
// async deleteOfflineTasksByKeyword(keyword) {
// try {
// const singInfo = await (async () => {
// const res = await gmHttp.get("https://115.com/?ct=offline&ac=space&_=" + (new Date).getTime());
// return "object" == typeof res ? res : null;
// })();
// if (!singInfo) {
// console.error("删除离线任务: 未登录115网盘");
// return;
// }
// const {sign: sign, time: time} = singInfo, uid = singInfo.uid || (() => {
// const match = document.cookie.match(/UID=(\d+)_/);
// return match ? match[1] : null;
// })() || await this.getUserId();
// const taskListRes = await gmHttp.postForm("https://115.com/web/lixian/?ct=lixian&ac=task_lists", {
// page: 1,
// uid: uid,
// sign: sign,
// time: time
// });
// const tasks = (null == taskListRes ? void 0 : taskListRes.tasks) || [];
// const keyword2 = keyword.toLowerCase().replace("fc2-", "");
// let deletedCount = 0;
// for (const task of tasks) {
// if (task.name && task.name.toLowerCase().includes(keyword2)) {
// console.log(`删除离线任务: ${task.name} (hash: ${task.info_hash})`);
// await gmHttp.postForm("https://115.com/web/lixian/?ct=lixian&ac=task_del", {
// "hash[0]": task.info_hash,
// flag: 1,
// uid: uid,
// sign: sign,
// time: time
// });
// deletedCount++;
// }
// }
// deletedCount > 0 && console.log(`共删除 ${deletedCount} 个离线任务`);
// } catch (error) {
// console.error("删除离线任务失败:", error);
// }
// }
}
+195 -1
View File
@@ -1,6 +1,7 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, currentHref, NO } from '../core/constants.js';
import { searchFiles } from './wangpan-115-task.js';
import {JavPackSubtitle} from './subtitles.js';
class WangPan115Plugin extends BasePlugin {
constructor() {
@@ -240,6 +241,19 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
event.stopPropagation();
await this.retryMatch(event.currentTarget);
}));
$(document).on("click", ".jhs-detail-delete-btn", (async event => {
event.preventDefault();
event.stopPropagation();
const $btn = $(event.currentTarget);
const matchData = $btn.attr("data-match");
if (!matchData) return;
const matchList = JSON.parse(matchData);
const carNum2 = this.getPageInfo().carNum;
this.showDeleteDialog(carNum2, matchList, (async newMatchList => {
$("#115-match-table").remove();
await this.matchDetailPage(newMatchList);
}));
}));
await this.matchDetailPage();
$(document).on("click", ".jhs-match-detail-error-btn", (async event => {
event.preventDefault();
@@ -266,6 +280,8 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
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("");
$tbody.append(rowsHtml);
$detail.css("position", "relative");
$detail.append(`<span class="jhs-detail-delete-btn" \n data-match='${JSON.stringify(matchList)}'\n style="position: absolute; top: 8px; right: 12px; cursor: pointer; color: #e74c3c; font-size: 16px; font-weight: bold; z-index: 1;" \n title="删除115作品">🗑️ 删除</span>`);
} else $tbody.append(`<tr><td colspan="4">\n <a class='jhs-match-detail-error-btn a-info'\n data-keyword="${carNum2}"\n title="未匹配,点击重试">未匹配</a>\n </td></tr>`);
} catch (error) {
$tbody.append(`<tr><td colspan="4">\n <a class="a-danger jhs-match-detail-error-btn" title="${error.message || "加载失败"}">加载失败,请重试</a>\n </td></tr>`);
@@ -341,7 +357,174 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
}
}
updateMatchStatus($box2, carNum2, matchList) {
matchList.length > 0 ? $box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-btn a-success' \n data-keyword="${carNum2}"\n data-match='${JSON.stringify(matchList)}'\n title="点击查看匹配详情">匹配${matchList.length}个</a>`) : $box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn a-info' data-keyword="${carNum2}" \n title="点击重新尝试匹配">未匹配</a>`);
if (matchList.length > 0) {
$box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-btn a-success' \n data-keyword="${carNum2}"\n data-match='${JSON.stringify(matchList)}'\n title="点击查看匹配详情">匹配${matchList.length}个</a>`);
const $deleteBtn = $box2.find(".delete115Svg");
const $markBtn = $box2.find(".markDeleteSvg");
if ($deleteBtn.length > 0 && matchList.some(m => m.dirId != null)) {
$deleteBtn.attr("data-match", JSON.stringify(matchList));
$deleteBtn.show();
}
if ($markBtn.length > 0) {
$markBtn.show();
this.updateMarkBtnStyle($markBtn, carNum2);
}
} else {
$box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn a-info' data-keyword="${carNum2}" \n title="点击重新尝试匹配">未匹配</a>`);
const $deleteBtn = $box2.find(".delete115Svg");
const $markBtn = $box2.find(".markDeleteSvg");
if ($deleteBtn.length > 0) {
$deleteBtn.removeAttr("data-match");
$deleteBtn.hide();
}
if ($markBtn.length > 0) {
$markBtn.hide();
}
}
}
async updateMarkBtnStyle($markBtn, carNum2) {
const markedList = await storageManager.forage.getItem("markedDeleteList") || [];
const isMarked = markedList.some(item => item.carNum === carNum2);
isMarked ? $markBtn.addClass("marked") : $markBtn.removeClass("marked");
$markBtn.attr("title", isMarked ? "取消标记删除" : "标记删除");
}
async batchDeleteMarked(selectedItems, onProgress) {
let deletedCount = 0;
let failedCount = 0;
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
for (let i = 0; i < selectedItems.length; i++) {
const item = selectedItems[i];
try {
const matchList = await this.searchFiles(item.carNum);
if (matchList.length === 0) {
failedCount++;
onProgress && onProgress(i + 1, selectedItems.length, item, "未匹配到115文件");
continue;
}
const upperCarNum = item.carNum.toUpperCase();
const safeMatchList = matchList.filter(m => m.name.toUpperCase().includes(upperCarNum));
if (safeMatchList.length === 0) {
failedCount++;
onProgress && onProgress(i + 1, selectedItems.length, item, "无安全可删除目录");
continue;
}
let itemDeleted = 0;
const uniqueDirIds = [...new Set(safeMatchList.map((m) => m.dirId))];
for (const dirId of uniqueDirIds) {
try {
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
"fid[0]": dirId,
ignore_warn: "1"
});
if (result && result.state) {
itemDeleted++;
deletedCount++;
}
} catch (e) {
console.error(`删除目录失败: ${dirId}`, e);
}
}
onProgress && onProgress(i + 1, selectedItems.length, item, itemDeleted > 0 ? `已删除${itemDeleted}个` : "删除失败");
if (i < selectedItems.length - 1) {
await delay(800);
}
} catch (error) {
failedCount++;
onProgress && onProgress(i + 1, selectedItems.length, item, error.message || "出错");
console.error(`批量删除出错 [${item.carNum}]:`, error);
}
}
return {deletedCount, failedCount};
}
showDeleteDialog(carNum2, matchList, afterDelete) {
const upperCarNum = carNum2.toUpperCase();
const safeMatchList = matchList.filter(m => m.name.toUpperCase().includes(upperCarNum));
const unsafeCount = matchList.length - safeMatchList.length;
if (safeMatchList.length === 0) {
show.error(`作品 "${carNum2}" 匹配到的 ${matchList.length} 个项目均不含番号,无法安全删除。请手动在115中删除。`);
return;
}
const formatSize = bytes => {
if (!bytes) return "-";
const units = ["B", "KB", "MB", "GB", "TB"];
let size = parseFloat(bytes), unit = 0;
for (; size >= 1024 && unit < units.length - 1;) size /= 1024, unit++;
return size.toFixed(2) + " " + units[unit];
};
let html = '<div style="padding: 15px; max-height: 400px; overflow-y: auto;">';
html += `<div style="margin-bottom: 12px; font-size: 14px;">作品 <b>${carNum2}</b> 匹配到 <b>${matchList.length}</b> 个文件夹(可删除 <b>${safeMatchList.length}</b> 个):</div>`;
if (unsafeCount > 0) {
html += `<div style="margin-bottom: 10px; padding: 6px 10px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; font-size: 12px; color: #856404;">⚠ 已自动排除 ${unsafeCount} 个目录名不含番号的项目,防止误删</div>`;
}
html += '<div style="margin-bottom: 10px;"><label style="cursor: pointer;"><input type="checkbox" class="jhs-delete-select-all" checked> <b>全选</b></label></div>';
for (let i = 0; i < safeMatchList.length; i++) {
const m = safeMatchList[i];
html += `<div style="margin-bottom: 6px; padding: 8px 10px; border: 1px solid #e0e0e0; border-radius: 4px; background: #fafafa;">
<label style="display: flex; align-items: flex-start; cursor: pointer; width: 100%;">
<input type="checkbox" class="jhs-delete-folder-cb" data-index="${i}" checked style="margin-top: 3px; flex-shrink: 0;">
<span style="margin-left: 8px; flex: 1;">
<div style="word-break: break-all;">${m.name}</div>
<div style="font-size: 12px; color: #888; margin-top: 2px;">${formatSize(m.size)} | ${m.createTime}</div>
</span>
</label>
</div>`;
}
html += "</div>";
const layerIndex = layer.open({
type: 1,
title: "删除115作品文件夹",
content: html,
area: ["520px", "auto"],
btn: ["确定删除", "取消"],
shadeClose: !1,
yes: async index => {
const $layer = $(`#layui-layer${index}`);
const checked = $layer.find(".jhs-delete-folder-cb:checked");
if (0 === checked.length) {
show.error("请至少选择一个文件夹");
return;
}
const selected = checked.map((function () {
return parseInt($(this).data("index"));
})).get();
layer.close(index);
let loadObj = loading();
let deletedCount = 0;
try {
const uniqueDirIds = [...new Set(selected.map((i) => safeMatchList[i].dirId))];
for (const dirId of uniqueDirIds) {
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
"fid[0]": dirId,
ignore_warn: "1"
});
result && result.state ? deletedCount++ : console.error(`删除目录失败: ${dirId}`, result);
}
loadObj.close();
show.ok(`作品 "${carNum2}" 已删除 ${deletedCount} 个文件夹`);
const newMatchList = await this.searchFiles(carNum2);
afterDelete && afterDelete(newMatchList);
const remainingCids = new Set(newMatchList.map((m) => m.dirId));
JavPackSubtitle.cached115Matches = JavPackSubtitle.cached115Matches.filter((m) => remainingCids.has(m.cid));
JavPackSubtitle.current115Cids = JavPackSubtitle.current115Cids.filter((cid) => remainingCids.has(cid));
try {
this.getBean("WangPan115TaskPlugin").deleteOfflineTasksByKeyword?.(carNum2);
} catch (e) {
console.warn("删除离线任务暂不可用:", e.message);
}
} catch (error) {
loadObj.close();
console.error("删除115文件失败:", error);
show.error(`删除失败: ${error.message || "网络错误"}`);
}
}
});
$(`#layui-layer${layerIndex}`).on("change", ".jhs-delete-select-all", (function () {
const checked = $(this).prop("checked");
$(`#layui-layer${layerIndex} .jhs-delete-folder-cb`).prop("checked", checked);
}));
}
async handleLoginRedirect() {
window.open("https://115.com");
@@ -351,6 +534,7 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
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)),
@@ -392,6 +576,16 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
if (!($box2.find("[class^='jhs-match-']").length > 0)) if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $box2.find(".video-title").prepend(`<a class='jhs-match-no-login-btn a-info' \n data-keyword="${carNum2}" \n title="未登录115网盘">未登录</a>`); else if (matchList.length > 0) {
const title = 1 === matchList.length ? "点击直接播放" : `点击查看${matchList.length}个匹配结果`;
$box2.find(".video-title").prepend(`<a class='jhs-match-btn a-success' \n data-keyword="${carNum2}"\n data-match='${JSON.stringify(matchList)}'\n title="${title}">匹配${matchList.length}个</a>`);
const $deleteBtn = $box2.find(".delete115Svg");
const $markBtn = $box2.find(".markDeleteSvg");
if ($deleteBtn.length > 0 && matchList.some(m => m.dirId != null)) {
$deleteBtn.attr("data-match", JSON.stringify(matchList));
$deleteBtn.show();
}
if ($markBtn.length > 0) {
$markBtn.show();
this.updateMarkBtnStyle($markBtn, carNum2);
}
} else $box2.find(".video-title").prepend(`<a class='jhs-match-error-btn a-info' \n data-keyword="${carNum2}" \n title="未匹配,点击重试">未匹配</a>`);
}
formatSize(bytes) {
+3 -4
View File
@@ -8,9 +8,9 @@ export default defineConfig({
userscript: {
name: 'JAV-JHS',
namespace: 'JAV-JHS',
version: '3.3.6-fix',
version: '3.3.7',
author: 'xie bro,fireinrain',
description: 'Jav-鉴黄师 收藏、屏蔽、标记已下载; 屏蔽标签、屏蔽演员、同步收藏演员、新作品检测; 免VIP查看热播、Top250排行榜、Fc2ppv、可查看所有评论信息、相关清单; 支持云盘备份; 以图识图; 字幕搜索; JavDb|JavBus',
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',
match: [
@@ -37,9 +37,8 @@ export default defineConfig({
'https://*seejav*/*actresses',
],
require: [
'https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js',
'https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js',
'https://cdn.jsdelivr.net/npm/tabulator-tables@6.3.1/dist/js/tabulator.min.js',
'https://cdn.jsdelivr.net/npm/tabulator-tables@6.5.2/dist/js/tabulator.min.js',
'https://cdn.jsdelivr.net/npm/layui-layer@1.0.9/dist/layer.min.js',
'https://cdn.jsdelivr.net/npm/blueimp-md5@2.19.0/js/md5.min.js',
'https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js',