6 Commits
v9 ... v11

Author SHA1 Message Date
lzyme.dev@gmail.com 7637d33dc6 更新文档 2026-08-30 13:45:02 +08:00
lzyme.dev@gmail.com 3287afe7ea 更新文档 2026-08-30 13:36:02 +08:00
lzyme.dev@gmail.com 06f7059f1b 新增一键删除风控策略 方式115触发风控 2026-08-30 13:27:06 +08:00
lzyme.dev@gmail.com c4d77e728a 新增一键清除提示
新增捐助地址
2026-08-30 13:14:30 +08:00
lzyme.dev@gmail.com a8fc1cde8a 新增磁力过滤与排序 2026-08-30 12:57:54 +08:00
lzyme.dev@gmail.com c8bcac5f98 修复javbus反复开启标题翻译失效 2026-08-30 11:27:07 +08:00
8 changed files with 1259 additions and 161 deletions
+119 -57
View File
@@ -39,15 +39,55 @@ git push → GitHub Actions 自动构建 → 发布 GitHub Release
## 功能概览
- **列表页增强**:作品状态标签(屏蔽/收藏/已下载/已观看)、一键筛选已收藏/新作品、演员黑名单过滤、标题关键词过滤
- **详情页增强**:磁力链接高亮匹配、DMM 多画质预览视频、标题翻译(Google 翻译)、字幕搜索(整合迅雷 + SubTitleCat,支持 115
网盘直传与多目录选择)、演员信息展示、相关清单、评论查看
- **多源预览图**:javfree / projectjav / javstore 三源预览图,支持来源切换、下载、新窗口打开,带 sessionStorage 缓存
- **115 网盘集成**:目录匹配与预加载、多目录选择上传、字幕一键直传
- **数据管理**:IndexedDB 本地存储、云盘备份/恢复(阿里云盘 / 115 网盘 / WebDAV)、跨 Tab 数据同步
- **自动化任务**:自动翻页、新作品检测、收藏演员同步、黑名单更新
- **UI 优化**:分类折叠、热榜 Top250、免 VIP 查看热播、以图识图、FC2 支持
- **多站点支持**:JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers / SubTitleCat
### 列表页增强
- 作品状态标签(屏蔽/收藏/已下载/已观看)
- 一键筛选已收藏/新作品/待鉴定
- 演员黑名单过滤、标题关键词过滤
- 封面快捷按钮(长缩略图/预览视频/鉴定按钮/第三方跳转/复制番号)
- 分类折叠、热榜 Top250、免 VIP 查看热播
- 自动翻页
### 详情页增强
- **磁力排序与过滤(JavDB)**:按发布日期/文件大小/文件数量拖拽排序,勾选启用;高清/4K/字幕/无码多条件并集过滤,4K 磁力行高亮显示
- 磁力链接高亮匹配已收藏/已下载状态
- DMM 多画质预览视频
- **标题翻译**:Google 翻译,支持开关多次切换不失效
- **字幕搜索**:合并迅雷 + SubTitleCat 为单一入口,支持 115 网盘直传、详情页预匹配目录、多目录选择上传、长目录名截断展示、字幕预览绿色字体
- **多源预览图**:javfree / projectjav / javstore 三源切换,来源按钮绿色高亮,sessionStorage 缓存,错误提示弹窗内显示
- 演员信息展示、相关清单、评论查看
- 以图识图、第三方站点跳转
### 115 网盘集成
- 作品目录匹配与详情页预加载
- 多目录选择上传字幕
- 字幕一键直传至匹配目录
- **作品删除增强**:删除整个文件夹(非仅视频),支持标记删除批量操作,删除后字幕目录列表联动;批量删除含风控保护(1~3s
随机延迟 + 触发风控自动重试退避)
### 数据管理
- IndexedDB 本地存储
- 云盘备份/恢复(阿里云盘 / WebDAV)
- 跨 Tab 数据同步(BroadcastChannel)
### 自动化任务
- 新作品检测
- 收藏演员同步
- 黑名单自动检测与更新
- 后台定时任务调度
### 其他
- FC2 站点支持(含 123AV 补全)
- MagnetHub 磁力搜索
- JavTrailers 站点处理
- 设置面板:快捷键配置、域名配置、缓存清理、标记删除列表、打赏作者(二维码/钱包地址可自定义)
- 浏览历史、想看/已看列表
- 多站点支持:JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers / SubTitleCat
---
@@ -59,63 +99,65 @@ git push → GitHub Actions 自动构建 → 发布 GitHub Release
```
JHS-enhance/
├── src/
│ ├── main.js # 入口文件:全局初始化、插件注册、页面启动
│ ├── core/ # 基础设施层
│ │ ├── constants.js # 全局常量、CSS 样式、页面类型判断
│ │ ├── base-plugin.js # 插件基类(含 SVG 图标、页面信息解析)
│ │ ├── plugin-manager.js # 插件管理器(注册、CSS/插件生命周期)
│ │ ├── globals.js # 全局依赖挂载(jQuery、localforage 等)
│ │ ├── storage.js # IndexedDB 存储管理器(localforage 封装)
│ │ ├── utils.js # 工具函数集(DOM 操作、弹窗、快捷键等)
│ │ └── gmHttp.js # GM_xmlhttpRequest 封装(带重试/并发控制)
│ ├── api/ # API 层
│ │ ├── javdb.js # JavDB API 封装(签名、搜索、评论等)
│ │ └── dmm.js # DMM 预览视频获取(多画质)
│ └── plugins/ # 插件层(40+ 个功能插件)
│ ├── list-page.js # 列表页过滤与状态标签
│ ├── list-page-button.js # 列表页功能按钮(待鉴定/已收藏/新作品/黑名单)
│ ├── detail-page.js # 详情页数据加载
│ ├── detail-page-button.js# 详情页操作按钮(含 115 目录预加载)
│ ├── setting.js # 设置面板(Tabulator 表格)
│ ├── translate.js # 标题翻译(Google 翻译 API)
│ ├── preview-video.js # DMM 预览视频
│ ├── highlight-magnet.js # 磁力链接高亮匹配
│ ├── auto-page.js # 自动翻页 / 云盘备份
│ ├── blacklist.js # 黑名单管理(演员/番号)
│ ├── favorite-actresses.js# 收藏演员同步
│ ├── new-video.js # 新作品检测
│ ├── task.js # 后台任务调度
│ ├── history.js # 浏览历史
│ ├── wangpan-115.js # 115 网盘匹配
│ ├── wangpan-115-task.js # 115 网盘任务处理
│ ├── aliyun-pan.js # 阿里云盘
│ ├── main.js # 入口文件:全局初始化、插件注册、页面启动、loading/Toast/Viewer 全局函数
│ ├── core/ # 基础设施层(7 个模块)
│ │ ├── constants.js # 全局常量、CSS 样式、页面类型判断、枚举值
│ │ ├── base-plugin.js # 插件基类(含 SVG 图标、页面信息解析、getBean 跨插件调用)
│ │ ├── plugin-manager.js # 插件管理器(注册、CSS 注入、插件生命周期、按站点分组)
│ │ ├── globals.js # 全局依赖挂载(jQuery、localforage、layer 等)
│ │ ├── storage.js # IndexedDB 存储管理器(localforage 封装,含缓存)
│ │ ├── utils.js # 工具函数集(DOM 操作、弹窗确认、快捷键、重试、日期格式化)
│ │ └── gmHttp.js # GM_xmlhttpRequest 封装(GET/POST/Form/File、超时重试、分块下载)
│ ├── api/ # API 层(2 个模块)
│ │ ├── javdb.js # JavDB API 封装(签名算法、搜索、评论、磁力列表)
│ │ └── dmm.js # DMM 预览视频获取(多画质、默认画质选择)
│ ├── lib/ # 第三方补丁库
│ │ └── gm-xhr-parallel.js # Tampermonkey MV3 并行请求修复(redirect: manual)
│ └── plugins/ # 插件层(41 个功能插件)
│ ├── list-page.js # 列表页过滤与状态标签渲染
│ ├── list-page-button.js # 列表页功能按钮(待鉴定/已收藏/新作品/黑名单管理)
│ ├── detail-page.js # 详情页数据加载与基础增强
│ ├── detail-page-button.js# 详情页操作按钮(115 目录预加载 + 磁力排序过滤面板)
│ ├── setting.js # 设置面板(Tab 页:基础/任务/域名/快捷键/屏蔽/缓存/标记删除/打赏作者)
│ ├── translate.js # 标题翻译(Google 翻译 API,原始标题缓存)
│ ├── preview-video.js # DMM 预览视频播放器
│ ├── highlight-magnet.js # 磁力链接高亮 + 排序 + 过滤(多键排序、并集过滤、4K 高亮)
│ ├── auto-page.js # 自动翻页
│ ├── blacklist.js # 黑名单管理(屏蔽演员/番号/关键词)
│ ├── favorite-actresses.js# 收藏演员同步与高亮
│ ├── new-video.js # 新作品检测与通知
│ ├── task.js # 后台任务调度(黑名单/收藏演员/新作品定时执行)
│ ├── history.js # 浏览历史表格
│ ├── wangpan-115.js # 115 网盘匹配/标签/删除(含风控保护的批量删除)
│ ├── wangpan-115-task.js # 115 网盘任务处理(文件搜索 API 封装)
│ ├── aliyun-pan.js # 阿里云盘备份/恢复
│ ├── fc2.js # FC2 页面处理
│ ├── fc2-by123av.js # FC2 通过 123AV 搜索
│ ├── actress-info.js # 演员信息展示
│ ├── related.js # 相关清单
│ ├── review.js # 评论展示
│ ├── cover-button.js # 封面按钮
│ ├── fold-category.js # 分类折叠
│ ├── hit-show.js # 热播榜单
│ ├── fc2-by123av.js # FC2 通过 123AV 补全番号信息
│ ├── actress-info.js # 演员信息展示(含头像、生日、三围等)
│ ├── related.js # 相关作品清单
│ ├── review.js # 评论区展示
│ ├── cover-button.js # 封面快捷按钮(SVG 图标 + 5 种封面操作)
│ ├── fold-category.js # 分类折叠展开
│ ├── hit-show.js # 热播榜单 + 免 VIP 查看
│ ├── top250.js # Top250 排行榜
│ ├── nav-bar.js # JavDB 导航栏增强
│ ├── bus-nav-bar.js # JavBus 导航栏增强
│ ├── bus-detail-page.js # JavBus 详情页
│ ├── bus-detail-page.js # JavBus 详情页专属处理
│ ├── bus-img.js # JavBus 图片处理
│ ├── bus-preview-video.js # JavBus 预览视频
│ ├── image-recognition.js # 以图识图
│ ├── screenshot.js # 多源预览图(javfree/projectjav/javstore)
│ ├── subtitles.js # 字幕搜索(整合迅雷+SubTitleCat)+ 115 直传
│ ├── req115.js # 115 网盘 API 封装(Req + Drive115 + Req115)
│ ├── image-recognition.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 字幕搜索(已整合到 subtitles.js,保留文件)
│ ├── other-site.js # 第三方站点跳转链接
│ ├── jav-trailers.js # JavTrailers 站点处理
│ ├── subtitle-cat.js # SubTitleCat 字幕搜索(已整合,保留原文件)
│ ├── filter-title-keyword.js # 标题关键词过滤
│ └── want-watched.js # 想看/已看视频列表
├── vite.config.mjs # Vite + vite-plugin-monkey 配置
├── vite.config.mjs # Vite + vite-plugin-monkey 配置(元数据/CDN 依赖/域名匹配)
├── package.json
└── JHS-origin.js # 原始单体脚本(参考)
└── JHS-origin.js # 原始单体脚本(参考归档)
```
---
@@ -245,6 +287,12 @@ window.show.ok('操作成功'); // Toast 通知
---
## 赞助
如果当前项目有帮到您的话,赞助我喝瓶水 :)
![赞助](https://i.imgur.com/ynGK6he.jpeg)
## 构建配置
`vite-plugin-monkey` 在 `vite.config.mjs` 中配置了:
@@ -258,7 +306,21 @@ window.show.ok('操作成功'); // Toast 通知
## 版本历史
- **v3.3.6** — 工程化重构版本,模块化拆分,支持 HMR 开发
- **v3.3.7**
- 新增:JavDB 磁力排序与过滤(拖拽排序、多条件并集过滤、4K 高亮)
- 新增:多源预览图(javfree/projectjav/javstore + 来源切换弹窗 + 绿色高亮)
- 新增:字幕搜索合并为单一入口(迅雷+SubTitleCat),详情页预匹配 115 目录,多目录选择上传
- 新增:标记删除批量 115 删除(含风控保护:1~3s 随机延迟 + 风控自动退避重试)
- 优化:作品删除改为删除整个文件夹(非仅视频),删除后字幕目录列表联动更新
- 修复:标题翻译开关多次切换后失效(原始标题 data-original-title 缓存)
- 修复:115 目录名过长截断展示(最多 26 字符)
- 修复:字幕预览区域字体改为绿色
- 修复:JavBus/JavDB 磁力列表 DOM 精确提取(data-rank/data-size/data-files/data-date 属性)
- 修复:无码过滤条件新增 -U/-UC/-u/-uc 匹配
- 优化:删除作品 115 风控提示(标记删除面板)
- 优化:设置面板打赏作者二维码/钱包地址可自定义
- **v3.3.6** — 工程化重构版本,模块化拆分(41 个插件),支持 Vite HMR 开发热更新
---
+570 -54
View File
@@ -631,6 +631,198 @@
background: rgba(59, 130, 246, 0.9);
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4);
}
.magnet-4k-highlight {
background-color: #fff5f5 !important;
}
.magnet-4k-highlight .name {
color: #f40 !important;
}
.msf-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 999997;
}
.msf-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
border-radius: 10px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
z-index: 999998;
width: 380px;
display: flex;
flex-direction: column;
}
.msf-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
border-radius: 10px 10px 0 0;
}
.msf-title {
font-size: 15px;
font-weight: 600;
color: #1e293b;
}
.msf-close {
font-size: 22px;
color: #9ca3af;
cursor: pointer;
padding: 0 6px;
line-height: 1;
transition: color 0.2s;
}
.msf-close:hover {
color: #ef4444;
}
.msf-body {
padding: 18px;
display: flex;
flex-direction: column;
gap: 16px;
}
.msf-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.msf-section-title {
font-size: 13px;
font-weight: 600;
color: #6b7280;
}
.msf-sort-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.msf-sort-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
cursor: default;
transition: all 0.2s;
user-select: none;
}
.msf-sort-item:hover {
border-color: #cbd5e1;
background: #f1f5f9;
}
.msf-sort-item.msf-dragging {
opacity: 0.5;
border-color: #3b82f6;
background: #eff6ff;
}
.msf-sort-item.msf-drag-over {
border-color: #3b82f6;
background: #eff6ff;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.msf-drag-handle {
color: #cbd5e1;
font-size: 16px;
cursor: grab;
line-height: 1;
letter-spacing: -2px;
transition: color 0.2s;
}
.msf-drag-handle:active {
cursor: grabbing;
}
.msf-sort-item:hover .msf-drag-handle {
color: #94a3b8;
}
.msf-sort-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #374151;
cursor: pointer;
flex: 1;
}
.msf-sort-checkbox {
margin: 0;
cursor: pointer;
width: 16px;
height: 16px;
}
.msf-sort-hint {
font-size: 12px;
color: #9ca3af;
margin-left: 4px;
}
.msf-filter-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.msf-filter-label {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 14px;
color: #374151;
cursor: pointer;
transition: all 0.2s;
user-select: none;
}
.msf-filter-label:hover {
border-color: #cbd5e1;
background: #f1f5f9;
}
.msf-filter-checkbox {
margin: 0;
cursor: pointer;
width: 16px;
height: 16px;
}
.msf-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 14px 18px;
border-top: 1px solid #e5e7eb;
}
.msf-btn {
padding: 8px 22px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.msf-btn-cancel {
background: #f3f4f6;
color: #374151;
}
.msf-btn-cancel:hover {
background: #e5e7eb;
}
.msf-btn-confirm {
background: #3b82f6;
color: #fff;
}
.msf-btn-confirm:hover {
background: #2563eb;
}
</style>
`;
const subtitleModalCss = `
@@ -3740,6 +3932,7 @@ ${value}\r
<div id="markDeleteList" style="max-height: 450px; overflow-y: auto; border: 1px solid #e0e0e0; border-radius: 4px;">
<div style="text-align: center; padding: 40px; color: #999;">加载中...</div>
</div>
<p style="color: #e74c3c; font-size: 12px; margin-top: 10px; text-align: left;">⚠ 作品删除目前暂无法删除115下载记录,如果需要重新下载,请去115云下载记录里删除或清空记录</p>
</div>
<!-- 快捷键 -->
@@ -3901,13 +4094,13 @@ ${value}\r
</div>
</div>
<div id="tip-author-panel" class="content-panel" style="display: ${"tip-author-panel" === defaultActivePanel ? "block" : "none"};">
<p style="color: #666; font-size: 0.9em;">如果JAV-JHS给您带来了便捷和价值,请考虑给予一点支持,您的鼓励是我持续创作的最大动力!感谢您的慷慨支持!</p>
<p style="color: #666; font-size: 0.9em;">如果JHS-enhance给您带来了便捷和价值,请考虑给予一点支持,您的鼓励是我持续创作的最大动力!感谢您的慷慨支持!</p>
<div>
<div style="display: flex; justify-content: space-around; align-items: flex-start; margin-bottom: 20px; flex-wrap: wrap;">
<div style="text-align: center; margin: 10px; flex: 1 1 30%; min-width: 150px;">
<img src="https://imgur.com/AvF0r3r.png" alt="TRC20-USDT二维码" style="width: 350px; height: 350px; border: 1px solid #ddd; padding: 5px; display: block; margin: 0 auto 5px;">
<p>TRC20-USDT</p>
<input type="text" readonly value="TYphgzpJ2hoDTa3J7kzj5xaHWbcPAyhbd5" onclick="this.select();document.execCommand('copy');alert('地址已复制!');"
<img src="https://i.imgur.com/KM5I6dD.jpeg" alt="SOL二维码" style="width: 350px; height: 350px; border: 1px solid #ddd; padding: 5px; display: block; margin: 0 auto 5px;">
<p>SOL</p>
<input type="text" readonly value="5xvRp1jhWkCasT1H7Nh8G5eAuvdCFD5s2Xux2w3rJ8RG" onclick="this.select();document.execCommand('copy');alert('地址已复制!');"
style="width: 90%; padding: 5px; margin-top: 5px; border: 1px solid #a99087; background-color: #fff; text-align: center; font-size: 0.8em; cursor: pointer; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;">
<p style="font-size: 0.75em; color: #5a504c; margin-top: 4px;">点击地址可复制</p>
</div>
@@ -8075,26 +8268,112 @@ ${value}\r
getName() {
return "HighlightMagnetPlugin";
}
doFilterMagnet() {
this.handleDb();
this.handleBus();
parseDateFromAttr(dateStr) {
if (!dateStr || dateStr.length !== 8) {
const d2 = new Date(dateStr);
return isNaN(d2.getTime()) ? 0 : d2.getTime();
}
const y = parseInt(dateStr.substring(0, 4), 10);
const m = parseInt(dateStr.substring(4, 6), 10) - 1;
const d = parseInt(dateStr.substring(6, 8), 10);
const date = new Date(y, m, d);
return isNaN(date.getTime()) ? 0 : date.getTime();
}
handleDb() {
getRowInfo(row) {
const $row = $(row);
const nameText = $row.find(".name").text().toLowerCase();
const tags = [];
$row.find(".tags .tag").each((_, tag) => tags.push($(tag).text().toLowerCase()));
const sizeMB = parseInt($row.attr("data-size"), 10) || 0;
const files = parseInt($row.attr("data-files"), 10) || 0;
const dateStr = $row.attr("data-date") || "";
return {
row,
$row,
nameText,
date: this.parseDateFromAttr(dateStr),
dateStr,
size: sizeMB * 1024 * 1024,
files,
tags,
is4K: nameText.includes("4k") || nameText.includes("[4k]") || tags.includes("4k"),
isHD: nameText.includes("高清") || tags.includes("高清"),
isSubtitle: nameText.includes("字幕") || nameText.includes("-c") || nameText.includes(".chs") || nameText.includes(".cht") || nameText.includes(".chi") || tags.includes("字幕"),
isUncensored: nameText.includes("无码") || nameText.includes("無碼") || nameText.includes("-u") || nameText.includes("-uc") || tags.includes("无码") || tags.includes("無碼")
};
}
async applySortAndFilter(settings) {
if (!isJavDb$1) return;
let magnetNameList = $("#magnets-content .name");
if (0 === magnetNameList.length) return;
const HIGH_QUALITY_KEYWORDS = ["4k", "-c", "-u", "-uc"];
let hasHighQuality = false;
magnetNameList.each(((_, el) => {
const $el = $(el), text = $el.text().toLowerCase(), isHighQuality = HIGH_QUALITY_KEYWORDS.some(((keyword) => text.includes(keyword)));
$el.parent().parent().parent().addClass("magnet-row");
text.includes("4k") && $el.css("color", "#f40");
if (isHighQuality) {
hasHighQuality = true;
$el.parent().parent().parent().addClass("high-quality");
}
}));
hasHighQuality ? $("#magnets-content .magnet-row").not(".high-quality").hide() : $("#enable-magnets-filter").addClass("do-hide");
const $container = $("#magnets-content");
const $rows = $container.find(".item[data-rank]");
if (0 === $rows.length) return;
const rowInfos = $rows.toArray().map((row) => this.getRowInfo(row));
const {filterHD, filter4K, filterSubtitle, filterUncensored, sortOrder} = settings;
const anyFilterEnabled = filterHD || filter4K || filterSubtitle || filterUncensored;
let visibleInfos = rowInfos;
if (anyFilterEnabled) {
visibleInfos = rowInfos.filter((info) => {
if (filterHD && info.isHD) return true;
if (filter4K && info.is4K) return true;
if (filterSubtitle && info.isSubtitle) return true;
if (filterUncensored && info.isUncensored) return true;
return false;
});
}
if (sortOrder && sortOrder.length > 0) {
visibleInfos.sort((a, b) => {
for (const key of sortOrder) {
let cmp = 0;
switch (key) {
case "date":
cmp = b.date - a.date;
break;
case "size":
cmp = b.size - a.size;
break;
case "files":
cmp = a.files - b.files;
break;
}
if (0 !== cmp) return cmp;
}
return 0;
});
}
const container = document.getElementById("magnets-content");
if (!container) return;
const hiddenInfos = rowInfos.filter((info) => !visibleInfos.includes(info));
const reviewsContainer = document.getElementById("reviewsContainer");
const insertRef = reviewsContainer && reviewsContainer.parentNode === container ? reviewsContainer.previousElementSibling || reviewsContainer : null;
const fragment = document.createDocumentFragment();
visibleInfos.forEach((info) => fragment.appendChild(info.row));
hiddenInfos.forEach((info) => fragment.appendChild(info.row));
if (insertRef) {
container.insertBefore(fragment, insertRef);
} else {
container.appendChild(fragment);
}
visibleInfos.forEach((info) => {
if (info.is4K) {
info.$row.addClass("magnet-4k-highlight");
}
});
hiddenInfos.forEach((info) => $(info.row).hide());
}
showAll() {
if (isJavDb$1) {
$("#magnets-content .item[data-rank]").toArray().forEach(((el) => {
$(el).show();
$(el).removeClass("magnet-4k-highlight");
}));
}
if (isJavBus$1) {
$("#magnet-table tr").toArray().forEach(((el) => $(el).show()));
}
}
handleBus() {
isJavBus$1 && window.isDetailPage && utils.loopDetector((() => $("#magnet-table td a").length > 0), (() => {
@@ -8114,12 +8393,6 @@ ${value}\r
})) : $("#enable-magnets-filter").addClass("do-hide");
}));
}
showAll() {
if (isJavDb$1) {
$("#magnets-content .item").toArray().forEach(((el) => $(el).show()));
}
isJavBus$1 && $("#magnet-table tr").toArray().forEach(((el) => $(el).show()));
}
}
class FoldCategoryPlugin extends BasePlugin {
getName() {
@@ -10714,10 +10987,13 @@ ${err.stack}` : "");
async batchDeleteMarked(selectedItems, onProgress) {
let deletedCount = 0;
let failedCount = 0;
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
const randomDelay = () => new Promise((resolve) => setTimeout(resolve, 1e3 + Math.random() * 2e3));
for (let i = 0; i < selectedItems.length; i++) {
const item = selectedItems[i];
try {
if (i > 0) {
await randomDelay();
}
const matchList = await this.searchFiles(item.carNum);
if (matchList.length === 0) {
failedCount++;
@@ -10733,24 +11009,49 @@ ${err.stack}` : "");
}
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++;
for (let d = 0; d < uniqueDirIds.length; d++) {
if (d > 0) {
await randomDelay();
}
const dirId = uniqueDirIds[d];
let retryCount = 0;
const maxRetries = 3;
while (retryCount <= maxRetries) {
try {
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
"fid[0]": dirId,
ignore_warn: "1"
});
if (result && result.state) {
itemDeleted++;
deletedCount++;
break;
} else {
const errMsg = result && result.error || "未知错误";
if (retryCount < maxRetries && this.isRateLimited(result)) {
retryCount++;
const backoff = (retryCount + 1) * 2e3 + Math.random() * 1e3;
onProgress && onProgress(i + 1, selectedItems.length, item, `风控触发,${backoff / 1e3}秒后重试(${retryCount}/${maxRetries})`);
await new Promise((resolve) => setTimeout(resolve, backoff));
continue;
}
console.error(`删除目录失败: ${dirId}`, errMsg);
break;
}
} catch (e) {
if (retryCount < maxRetries && this.isRateLimited(e)) {
retryCount++;
const backoff = (retryCount + 1) * 2e3 + Math.random() * 1e3;
onProgress && onProgress(i + 1, selectedItems.length, item, `风控触发,${backoff / 1e3}秒后重试(${retryCount}/${maxRetries})`);
await new Promise((resolve) => setTimeout(resolve, backoff));
continue;
}
console.error(`删除目录失败: ${dirId}`, e);
break;
}
} 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 || "出错");
@@ -10759,6 +11060,20 @@ ${err.stack}` : "");
}
return {deletedCount, failedCount};
}
isRateLimited(response) {
if (!response) return false;
if (response.state === false) {
const msg = (response.error || response.msg || "").toLowerCase();
if (msg.includes("频繁") || msg.includes("操作太频繁") || msg.includes("稍后再试") || msg.includes("rate") || msg.includes("limit") || msg.includes("too many")) {
return true;
}
}
if (response.status === 403 || response.status === 429) {
return true;
}
return false;
}
showDeleteDialog(carNum2, matchList, afterDelete) {
const upperCarNum = carNum2.toUpperCase();
const safeMatchList = matchList.filter((m) => m.name.toUpperCase().includes(upperCarNum));
@@ -12896,7 +13211,7 @@ ${err.stack}` : "");
}
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="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 ';
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$1 && $(".tabs").after(buttonsHtml);
isJavBus$1 && $("#mag-submit-show").before(buttonsHtml);
$("#favoriteBtn").on("click", (() => this.favoriteOne()));
@@ -12916,19 +13231,38 @@ ${err.stack}` : "");
}
});
}));
const highlightMagnetPlugin = this.getBean("HighlightMagnetPlugin"), enableMagnetsFilter = await storageManager.getSetting("enableMagnetsFilter", YES);
$("#magnets-span").text(enableMagnetsFilter === YES ? "关闭磁力过滤" : "开启磁力过滤");
enableMagnetsFilter === YES && highlightMagnetPlugin.doFilterMagnet();
const highlightMagnetPlugin = this.getBean("HighlightMagnetPlugin"),
enableMagnetsFilter = await storageManager.getSetting("enableMagnetsFilter", NO);
if (enableMagnetsFilter === YES) {
const savedSettings = await storageManager.getSetting("magnetSortAndFilter", "{}");
let settings;
try {
settings = JSON.parse(savedSettings);
} catch (e) {
settings = {};
}
const sortEnabled = settings.sortEnabled || {};
const fullSortOrder = settings.sortOrder && settings.sortOrder.length === 3 ? settings.sortOrder : ["date", "size", "files"];
const effectiveSortOrder = fullSortOrder.filter((key) => sortEnabled[key]);
highlightMagnetPlugin.applySortAndFilter({
filterHD: settings.filterHD || false,
filter4K: settings.filter4K || false,
filterSubtitle: settings.filterSubtitle || false,
filterUncensored: settings.filterUncensored || false,
sortOrder: effectiveSortOrder
});
$("#magnets-span").text("取消磁力过滤");
} else {
$("#magnets-span").text("排序与过滤");
}
$("#enable-magnets-filter").on("click", ((event) => {
let $span = $("#magnets-span");
if ("关闭磁力过滤" === $span.text()) {
const $span = $("#magnets-span");
if ("取消磁力过滤" === $span.text()) {
highlightMagnetPlugin.showAll();
$span.text("开启磁力过滤");
$span.text("排序与过滤");
storageManager.saveSettingItem("enableMagnetsFilter", NO);
} else {
highlightMagnetPlugin.doFilterMagnet();
$span.text("关闭磁力过滤");
storageManager.saveSettingItem("enableMagnetsFilter", YES);
this.openMagnetSortFilterPanel(highlightMagnetPlugin);
}
}));
$("#subtitleSearchBtn").on("click", (() => {
@@ -12937,6 +13271,186 @@ ${err.stack}` : "");
window.JavPackSubtitle.preload115Matches(carNum2);
this.showStatus(carNum2).then();
}
async openMagnetSortFilterPanel(highlightMagnetPlugin) {
const existingPanel = document.getElementById("magnet-sort-filter-panel");
if (existingPanel) {
existingPanel.remove();
return;
}
const savedSettings = await storageManager.getSetting("magnetSortAndFilter", "{}");
let settings;
try {
settings = JSON.parse(savedSettings);
} catch (e) {
settings = {};
}
const sortEnabled = settings.sortEnabled || {date: false, size: false, files: false};
const sortOrder = settings.sortOrder && settings.sortOrder.length === 3 ? settings.sortOrder : ["date", "size", "files"];
const filterHD = settings.filterHD || false;
const filter4K = settings.filter4K || false;
const filterSubtitle = settings.filterSubtitle || false;
const filterUncensored = settings.filterUncensored || false;
const SORT_LABELS = {date: "发布日期", size: "文件大小", files: "文件数量"};
const SORT_HINTS = {date: "从新到旧", size: "从大到小", files: "从少到多"};
const FILTER_ITEMS = [
{key: "HD", label: "高清", checked: filterHD},
{key: "4K", label: "4K", checked: filter4K},
{key: "Subtitle", label: "字幕", checked: filterSubtitle},
{key: "Uncensored", label: "无码", checked: filterUncensored}
];
const currentSortOrder = [...sortOrder];
const currentSortEnabled = {...sortEnabled};
const currentFilters = {
HD: filterHD,
"4K": filter4K,
Subtitle: filterSubtitle,
Uncensored: filterUncensored
};
const buildSortItemsHTML = () => {
return currentSortOrder.map((key) => {
const checked = currentSortEnabled[key] ? "checked" : "";
return `<div class="msf-sort-item" draggable="true" data-sort-key="${key}">
<span class="msf-drag-handle">⋮⋮</span>
<label class="msf-sort-label">
<input type="checkbox" class="msf-sort-checkbox" data-sort-key="${key}" ${checked}>
${SORT_LABELS[key]} <span class="msf-sort-hint">${SORT_HINTS[key]}</span>
</label>
</div>`;
}).join("");
};
const buildFilterItemsHTML = () => {
return FILTER_ITEMS.map((item) => {
const checked = currentFilters[item.key] ? "checked" : "";
return `<label class="msf-filter-label">
<input type="checkbox" class="msf-filter-checkbox" data-filter-key="${item.key}" ${checked}>
${item.label}
</label>`;
}).join("");
};
const panelHTML = `
<div class="msf-overlay" id="msf-overlay"></div>
<div class="msf-panel" id="magnet-sort-filter-panel">
<div class="msf-header">
<span class="msf-title">磁力排序与过滤</span>
<span class="msf-close" id="msf-close-btn">&times;</span>
</div>
<div class="msf-body">
<div class="msf-section">
<div class="msf-section-title">排序优先级(拖动调整顺序,勾选启用)</div>
<div class="msf-sort-list" id="msf-sort-list">
${buildSortItemsHTML()}
</div>
</div>
<div class="msf-section">
<div class="msf-section-title">过滤条件(可多选,并集关系)</div>
<div class="msf-filter-list">
${buildFilterItemsHTML()}
</div>
</div>
</div>
<div class="msf-footer">
<button class="msf-btn msf-btn-cancel" id="msf-cancel-btn">取消</button>
<button class="msf-btn msf-btn-confirm" id="msf-confirm-btn">确认</button>
</div>
</div>
`;
const wrapper = document.createElement("div");
wrapper.innerHTML = panelHTML;
document.body.appendChild(wrapper);
const panel = document.getElementById("magnet-sort-filter-panel");
const overlay = document.getElementById("msf-overlay");
const closePanel = () => {
panel.remove();
overlay.remove();
};
overlay.addEventListener("click", closePanel);
document.getElementById("msf-close-btn").addEventListener("click", closePanel);
document.getElementById("msf-cancel-btn").addEventListener("click", closePanel);
const rebuildSortList = () => {
const sortList = document.getElementById("msf-sort-list");
sortList.innerHTML = buildSortItemsHTML();
bindSortEvents();
};
const bindSortEvents = () => {
const sortItems = document.querySelectorAll("#msf-sort-list .msf-sort-item");
sortItems.forEach((item) => {
item.addEventListener("dragstart", (e) => {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", item.dataset.sortKey);
item.classList.add("msf-dragging");
});
item.addEventListener("dragend", () => {
item.classList.remove("msf-dragging");
document.querySelectorAll("#msf-sort-list .msf-sort-item").forEach((el) => el.classList.remove("msf-drag-over"));
});
item.addEventListener("dragover", (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
item.classList.add("msf-drag-over");
});
item.addEventListener("dragleave", () => {
item.classList.remove("msf-drag-over");
});
item.addEventListener("drop", (e) => {
e.preventDefault();
item.classList.remove("msf-drag-over");
const fromKey = e.dataTransfer.getData("text/plain");
const toKey = item.dataset.sortKey;
if (fromKey === toKey) return;
const fromIdx = currentSortOrder.indexOf(fromKey);
const toIdx = currentSortOrder.indexOf(toKey);
currentSortOrder.splice(fromIdx, 1);
currentSortOrder.splice(toIdx, 0, fromKey);
rebuildSortList();
});
const checkbox = item.querySelector(".msf-sort-checkbox");
checkbox.addEventListener("change", () => {
currentSortEnabled[item.dataset.sortKey] = checkbox.checked;
});
});
};
bindSortEvents();
document.querySelectorAll(".msf-filter-checkbox").forEach((cb) => {
cb.addEventListener("change", () => {
currentFilters[cb.dataset.filterKey] = cb.checked;
});
});
document.getElementById("msf-confirm-btn").addEventListener("click", async () => {
try {
const enabledSortOrder = currentSortOrder.filter((key) => currentSortEnabled[key]);
const newSettings = {
sortOrder: currentSortOrder,
sortEnabled: currentSortEnabled,
filterHD: currentFilters.HD,
filter4K: currentFilters["4K"],
filterSubtitle: currentFilters.Subtitle,
filterUncensored: currentFilters.Uncensored
};
await storageManager.saveSettingItem("magnetSortAndFilter", JSON.stringify(newSettings));
const hasAnyFilter = currentFilters.HD || currentFilters["4K"] || currentFilters.Subtitle || currentFilters.Uncensored;
const hasAnySort = enabledSortOrder.length > 0;
if (hasAnyFilter || hasAnySort) {
await highlightMagnetPlugin.applySortAndFilter({
filterHD: currentFilters.HD,
filter4K: currentFilters["4K"],
filterSubtitle: currentFilters.Subtitle,
filterUncensored: currentFilters.Uncensored,
sortOrder: enabledSortOrder
});
$("#magnets-span").text("取消磁力过滤");
await storageManager.saveSettingItem("enableMagnetsFilter", YES);
} else {
highlightMagnetPlugin.showAll();
$("#magnets-span").text("排序与过滤");
await storageManager.saveSettingItem("enableMagnetsFilter", NO);
}
} catch (e) {
console.error("[磁力排序] 应用失败:", e);
}
closePanel();
});
}
async showStatus(carNum2) {
const $filterBtn = $("#filterBtn span"), $favoriteBtn = $("#favoriteBtn span"), $hasDownBtn = $("#hasDownBtn span"), $hasWatchBtn = $("#hasWatchBtn span"), hotKeyDisplay = (hotKey) => hotKey ? `(${hotKey})` : "";
$filterBtn.text(`🚫 屏蔽 ${hotKeyDisplay(this.filterHotKey)}`);
@@ -14300,11 +14814,13 @@ ${err.stack}` : "");
}
if (this.cache[carNum2]) {
let _this = this;
$title.attr("data-original-title", content);
$title.contents().each((function() {
3 === this.nodeType && "" !== this.textContent.trim() && (this.textContent = " " + _this.cache[carNum2] + " ");
}));
$title.attr("title", _this.cache[carNum2]);
} else translateText(content).then(((result) => {
$title.attr("data-original-title", content);
if (isJavDb$1) {
$title.contents().each((function() {
3 !== this.nodeType || "" === this.textContent.trim() || this.textContent.includes(carNum2) || (this.textContent = " " + result + " ");
@@ -14322,7 +14838,7 @@ ${err.stack}` : "");
async revertTranslation() {
$(this.getSelector().itemSelector).toArray().forEach(((ele) => {
let $box2 = $(ele);
const originalContent = $box2.find(".box").attr("title") || $box2.find(".video-title").attr("title") || $box2.find("img").attr("data-title");
const originalContent = $box2.find(".video-title").attr("data-original-title") || $box2.find(".box").attr("title") || $box2.find(".video-title").attr("title") || $box2.find("img").attr("data-title");
let carNum2;
isJavDb$1 && (carNum2 = $box2.find(".video-title strong").text().trim());
const $title = $box2.find(".video-title");
+192
View File
@@ -569,6 +569,198 @@ const mainCss = `
background: rgba(59, 130, 246, 0.9);
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4);
}
.magnet-4k-highlight {
background-color: #fff5f5 !important;
}
.magnet-4k-highlight .name {
color: #f40 !important;
}
.msf-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 999997;
}
.msf-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
border-radius: 10px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
z-index: 999998;
width: 380px;
display: flex;
flex-direction: column;
}
.msf-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
border-radius: 10px 10px 0 0;
}
.msf-title {
font-size: 15px;
font-weight: 600;
color: #1e293b;
}
.msf-close {
font-size: 22px;
color: #9ca3af;
cursor: pointer;
padding: 0 6px;
line-height: 1;
transition: color 0.2s;
}
.msf-close:hover {
color: #ef4444;
}
.msf-body {
padding: 18px;
display: flex;
flex-direction: column;
gap: 16px;
}
.msf-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.msf-section-title {
font-size: 13px;
font-weight: 600;
color: #6b7280;
}
.msf-sort-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.msf-sort-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
cursor: default;
transition: all 0.2s;
user-select: none;
}
.msf-sort-item:hover {
border-color: #cbd5e1;
background: #f1f5f9;
}
.msf-sort-item.msf-dragging {
opacity: 0.5;
border-color: #3b82f6;
background: #eff6ff;
}
.msf-sort-item.msf-drag-over {
border-color: #3b82f6;
background: #eff6ff;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.msf-drag-handle {
color: #cbd5e1;
font-size: 16px;
cursor: grab;
line-height: 1;
letter-spacing: -2px;
transition: color 0.2s;
}
.msf-drag-handle:active {
cursor: grabbing;
}
.msf-sort-item:hover .msf-drag-handle {
color: #94a3b8;
}
.msf-sort-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #374151;
cursor: pointer;
flex: 1;
}
.msf-sort-checkbox {
margin: 0;
cursor: pointer;
width: 16px;
height: 16px;
}
.msf-sort-hint {
font-size: 12px;
color: #9ca3af;
margin-left: 4px;
}
.msf-filter-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.msf-filter-label {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 14px;
color: #374151;
cursor: pointer;
transition: all 0.2s;
user-select: none;
}
.msf-filter-label:hover {
border-color: #cbd5e1;
background: #f1f5f9;
}
.msf-filter-checkbox {
margin: 0;
cursor: pointer;
width: 16px;
height: 16px;
}
.msf-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 14px 18px;
border-top: 1px solid #e5e7eb;
}
.msf-btn {
padding: 8px 22px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.msf-btn-cancel {
background: #f3f4f6;
color: #374151;
}
.msf-btn-cancel:hover {
background: #e5e7eb;
}
.msf-btn-confirm {
background: #3b82f6;
color: #fff;
}
.msf-btn-confirm:hover {
background: #2563eb;
}
</style>
`;
+209 -10
View File
@@ -23,7 +23,7 @@ class DetailPageButtonPlugin extends BasePlugin {
}
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="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 ';
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()));
@@ -43,19 +43,38 @@ class DetailPageButtonPlugin extends BasePlugin {
}
});
}));
const highlightMagnetPlugin = this.getBean("HighlightMagnetPlugin"), enableMagnetsFilter = await storageManager.getSetting("enableMagnetsFilter", YES);
$("#magnets-span").text(enableMagnetsFilter === YES ? "关闭磁力过滤" : "开启磁力过滤");
enableMagnetsFilter === YES && highlightMagnetPlugin.doFilterMagnet();
const highlightMagnetPlugin = this.getBean("HighlightMagnetPlugin"),
enableMagnetsFilter = await storageManager.getSetting("enableMagnetsFilter", NO);
if (enableMagnetsFilter === YES) {
const savedSettings = await storageManager.getSetting("magnetSortAndFilter", "{}");
let settings;
try {
settings = JSON.parse(savedSettings);
} catch (e) {
settings = {};
}
const sortEnabled = settings.sortEnabled || {};
const fullSortOrder = (settings.sortOrder && settings.sortOrder.length === 3) ? settings.sortOrder : ["date", "size", "files"];
const effectiveSortOrder = fullSortOrder.filter(key => sortEnabled[key]);
highlightMagnetPlugin.applySortAndFilter({
filterHD: settings.filterHD || !1,
filter4K: settings.filter4K || !1,
filterSubtitle: settings.filterSubtitle || !1,
filterUncensored: settings.filterUncensored || !1,
sortOrder: effectiveSortOrder,
});
$("#magnets-span").text("取消磁力过滤");
} else {
$("#magnets-span").text("排序与过滤");
}
$("#enable-magnets-filter").on("click", (event => {
let $span = $("#magnets-span");
if ("关闭磁力过滤" === $span.text()) {
const $span = $("#magnets-span");
if ("取消磁力过滤" === $span.text()) {
highlightMagnetPlugin.showAll();
$span.text("开启磁力过滤");
$span.text("排序与过滤");
storageManager.saveSettingItem("enableMagnetsFilter", NO);
} else {
highlightMagnetPlugin.doFilterMagnet();
$span.text("关闭磁力过滤");
storageManager.saveSettingItem("enableMagnetsFilter", YES);
this.openMagnetSortFilterPanel(highlightMagnetPlugin);
}
}));
$("#subtitleSearchBtn").on("click", (() => {
@@ -66,6 +85,186 @@ class DetailPageButtonPlugin extends BasePlugin {
// $("#xunLeiSubtitleBtn").on("click", (() => this.searchXunLeiSubtitle(carNum2)));
this.showStatus(carNum2).then();
}
async openMagnetSortFilterPanel(highlightMagnetPlugin) {
const existingPanel = document.getElementById("magnet-sort-filter-panel");
if (existingPanel) {
existingPanel.remove();
return;
}
const savedSettings = await storageManager.getSetting("magnetSortAndFilter", "{}");
let settings;
try {
settings = JSON.parse(savedSettings);
} catch (e) {
settings = {};
}
const sortEnabled = settings.sortEnabled || {date: !1, size: !1, files: !1};
const sortOrder = (settings.sortOrder && settings.sortOrder.length === 3) ? settings.sortOrder : ["date", "size", "files"];
const filterHD = settings.filterHD || !1;
const filter4K = settings.filter4K || !1;
const filterSubtitle = settings.filterSubtitle || !1;
const filterUncensored = settings.filterUncensored || !1;
const SORT_LABELS = {date: "发布日期", size: "文件大小", files: "文件数量"};
const SORT_HINTS = {date: "从新到旧", size: "从大到小", files: "从少到多"};
const FILTER_ITEMS = [
{key: "HD", label: "高清", checked: filterHD},
{key: "4K", label: "4K", checked: filter4K},
{key: "Subtitle", label: "字幕", checked: filterSubtitle},
{key: "Uncensored", label: "无码", checked: filterUncensored},
];
const currentSortOrder = [...sortOrder];
const currentSortEnabled = {...sortEnabled};
const currentFilters = {
HD: filterHD,
"4K": filter4K,
Subtitle: filterSubtitle,
Uncensored: filterUncensored,
};
const buildSortItemsHTML = () => {
return currentSortOrder.map(key => {
const checked = currentSortEnabled[key] ? "checked" : "";
return `<div class="msf-sort-item" draggable="true" data-sort-key="${key}">
<span class="msf-drag-handle">⋮⋮</span>
<label class="msf-sort-label">
<input type="checkbox" class="msf-sort-checkbox" data-sort-key="${key}" ${checked}>
${SORT_LABELS[key]} <span class="msf-sort-hint">${SORT_HINTS[key]}</span>
</label>
</div>`;
}).join("");
};
const buildFilterItemsHTML = () => {
return FILTER_ITEMS.map(item => {
const checked = currentFilters[item.key] ? "checked" : "";
return `<label class="msf-filter-label">
<input type="checkbox" class="msf-filter-checkbox" data-filter-key="${item.key}" ${checked}>
${item.label}
</label>`;
}).join("");
};
const panelHTML = `
<div class="msf-overlay" id="msf-overlay"></div>
<div class="msf-panel" id="magnet-sort-filter-panel">
<div class="msf-header">
<span class="msf-title">磁力排序与过滤</span>
<span class="msf-close" id="msf-close-btn">&times;</span>
</div>
<div class="msf-body">
<div class="msf-section">
<div class="msf-section-title">排序优先级(拖动调整顺序,勾选启用)</div>
<div class="msf-sort-list" id="msf-sort-list">
${buildSortItemsHTML()}
</div>
</div>
<div class="msf-section">
<div class="msf-section-title">过滤条件(可多选,并集关系)</div>
<div class="msf-filter-list">
${buildFilterItemsHTML()}
</div>
</div>
</div>
<div class="msf-footer">
<button class="msf-btn msf-btn-cancel" id="msf-cancel-btn">取消</button>
<button class="msf-btn msf-btn-confirm" id="msf-confirm-btn">确认</button>
</div>
</div>
`;
const wrapper = document.createElement("div");
wrapper.innerHTML = panelHTML;
document.body.appendChild(wrapper);
const panel = document.getElementById("magnet-sort-filter-panel");
const overlay = document.getElementById("msf-overlay");
const closePanel = () => {
panel.remove();
overlay.remove();
};
overlay.addEventListener("click", closePanel);
document.getElementById("msf-close-btn").addEventListener("click", closePanel);
document.getElementById("msf-cancel-btn").addEventListener("click", closePanel);
const rebuildSortList = () => {
const sortList = document.getElementById("msf-sort-list");
sortList.innerHTML = buildSortItemsHTML();
bindSortEvents();
};
const bindSortEvents = () => {
const sortItems = document.querySelectorAll("#msf-sort-list .msf-sort-item");
sortItems.forEach(item => {
item.addEventListener("dragstart", (e) => {
e.dataTransfer.effectAllowed = "move";
e.dataTransfer.setData("text/plain", item.dataset.sortKey);
item.classList.add("msf-dragging");
});
item.addEventListener("dragend", () => {
item.classList.remove("msf-dragging");
document.querySelectorAll("#msf-sort-list .msf-sort-item").forEach(el => el.classList.remove("msf-drag-over"));
});
item.addEventListener("dragover", (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = "move";
item.classList.add("msf-drag-over");
});
item.addEventListener("dragleave", () => {
item.classList.remove("msf-drag-over");
});
item.addEventListener("drop", (e) => {
e.preventDefault();
item.classList.remove("msf-drag-over");
const fromKey = e.dataTransfer.getData("text/plain");
const toKey = item.dataset.sortKey;
if (fromKey === toKey) return;
const fromIdx = currentSortOrder.indexOf(fromKey);
const toIdx = currentSortOrder.indexOf(toKey);
currentSortOrder.splice(fromIdx, 1);
currentSortOrder.splice(toIdx, 0, fromKey);
rebuildSortList();
});
const checkbox = item.querySelector(".msf-sort-checkbox");
checkbox.addEventListener("change", () => {
currentSortEnabled[item.dataset.sortKey] = checkbox.checked;
});
});
};
bindSortEvents();
document.querySelectorAll(".msf-filter-checkbox").forEach(cb => {
cb.addEventListener("change", () => {
currentFilters[cb.dataset.filterKey] = cb.checked;
});
});
document.getElementById("msf-confirm-btn").addEventListener("click", async () => {
try {
const enabledSortOrder = currentSortOrder.filter(key => currentSortEnabled[key]);
const newSettings = {
sortOrder: currentSortOrder,
sortEnabled: currentSortEnabled,
filterHD: currentFilters.HD,
filter4K: currentFilters["4K"],
filterSubtitle: currentFilters.Subtitle,
filterUncensored: currentFilters.Uncensored,
};
await storageManager.saveSettingItem("magnetSortAndFilter", JSON.stringify(newSettings));
const hasAnyFilter = currentFilters.HD || currentFilters["4K"] || currentFilters.Subtitle || currentFilters.Uncensored;
const hasAnySort = enabledSortOrder.length > 0;
if (hasAnyFilter || hasAnySort) {
await highlightMagnetPlugin.applySortAndFilter({
filterHD: currentFilters.HD,
filter4K: currentFilters["4K"],
filterSubtitle: currentFilters.Subtitle,
filterUncensored: currentFilters.Uncensored,
sortOrder: enabledSortOrder,
});
$("#magnets-span").text("取消磁力过滤");
await storageManager.saveSettingItem("enableMagnetsFilter", YES);
} else {
highlightMagnetPlugin.showAll();
$("#magnets-span").text("排序与过滤");
await storageManager.saveSettingItem("enableMagnetsFilter", NO);
}
} catch (e) {
console.error("[磁力排序] 应用失败:", e);
}
closePanel();
});
}
async showStatus(carNum2) {
const $filterBtn = $("#filterBtn span"), $favoriteBtn = $("#favoriteBtn span"), $hasDownBtn = $("#hasDownBtn span"), $hasWatchBtn = $("#hasWatchBtn span"), hotKeyDisplay = hotKey => hotKey ? `(${hotKey})` : "";
$filterBtn.text(`🚫 屏蔽 ${hotKeyDisplay(this.filterHotKey)}`);
+105 -23
View File
@@ -5,26 +5,114 @@ class HighlightMagnetPlugin extends BasePlugin {
getName() {
return "HighlightMagnetPlugin";
}
doFilterMagnet() {
this.handleDb();
this.handleBus();
parseDateFromAttr(dateStr) {
if (!dateStr || dateStr.length !== 8) {
const d = new Date(dateStr);
return isNaN(d.getTime()) ? 0 : d.getTime();
}
const y = parseInt(dateStr.substring(0, 4), 10);
const m = parseInt(dateStr.substring(4, 6), 10) - 1;
const d = parseInt(dateStr.substring(6, 8), 10);
const date = new Date(y, m, d);
return isNaN(date.getTime()) ? 0 : date.getTime();
}
handleDb() {
getRowInfo(row) {
const $row = $(row);
const nameText = $row.find('.name').text().toLowerCase();
const tags = [];
$row.find('.tags .tag').each((_, tag) => tags.push($(tag).text().toLowerCase()));
const sizeMB = parseInt($row.attr('data-size'), 10) || 0;
const files = parseInt($row.attr('data-files'), 10) || 0;
const dateStr = $row.attr('data-date') || '';
return {
row: row,
$row: $row,
nameText: nameText,
date: this.parseDateFromAttr(dateStr),
dateStr: dateStr,
size: sizeMB * 1024 * 1024,
files: files,
tags: tags,
is4K: nameText.includes('4k') || nameText.includes('[4k]') || tags.includes('4k'),
isHD: nameText.includes('高清') || tags.includes('高清'),
isSubtitle: nameText.includes('字幕') || nameText.includes('-c') || nameText.includes('.chs') || nameText.includes('.cht') || nameText.includes('.chi') || tags.includes('字幕'),
isUncensored: nameText.includes('无码') || nameText.includes('無碼') || nameText.includes('-u') || nameText.includes('-uc') || tags.includes('无码') || tags.includes('無碼'),
};
}
async applySortAndFilter(settings) {
if (!isJavDb) return;
let magnetNameList = $("#magnets-content .name");
if (0 === magnetNameList.length) return;
const HIGH_QUALITY_KEYWORDS = [ "4k", "-c", "-u", "-uc" ];
let hasHighQuality = !1;
magnetNameList.each(((_, el) => {
const $el = $(el), text = $el.text().toLowerCase(), isHighQuality = HIGH_QUALITY_KEYWORDS.some((keyword => text.includes(keyword)));
$el.parent().parent().parent().addClass("magnet-row");
text.includes("4k") && $el.css("color", "#f40");
if (isHighQuality) {
hasHighQuality = !0;
$el.parent().parent().parent().addClass("high-quality");
const $container = $("#magnets-content");
const $rows = $container.find(".item[data-rank]");
if (0 === $rows.length) return;
const rowInfos = $rows.toArray().map(row => this.getRowInfo(row));
const {filterHD, filter4K, filterSubtitle, filterUncensored, sortOrder} = settings;
const anyFilterEnabled = filterHD || filter4K || filterSubtitle || filterUncensored;
let visibleInfos = rowInfos;
if (anyFilterEnabled) {
visibleInfos = rowInfos.filter(info => {
if (filterHD && info.isHD) return !0;
if (filter4K && info.is4K) return !0;
if (filterSubtitle && info.isSubtitle) return !0;
if (filterUncensored && info.isUncensored) return !0;
return !1;
});
}
if (sortOrder && sortOrder.length > 0) {
visibleInfos.sort((a, b) => {
for (const key of sortOrder) {
let cmp = 0;
switch (key) {
case 'date':
cmp = b.date - a.date;
break;
case 'size':
cmp = b.size - a.size;
break;
case 'files':
cmp = a.files - b.files;
break;
}
if (0 !== cmp) return cmp;
}
return 0;
});
}
const container = document.getElementById("magnets-content");
if (!container) return;
const hiddenInfos = rowInfos.filter(info => !visibleInfos.includes(info));
const reviewsContainer = document.getElementById("reviewsContainer");
const insertRef = reviewsContainer && reviewsContainer.parentNode === container
? (reviewsContainer.previousElementSibling || reviewsContainer)
: null;
const fragment = document.createDocumentFragment();
visibleInfos.forEach(info => fragment.appendChild(info.row));
hiddenInfos.forEach(info => fragment.appendChild(info.row));
if (insertRef) {
container.insertBefore(fragment, insertRef);
} else {
container.appendChild(fragment);
}
visibleInfos.forEach(info => {
if (info.is4K) {
info.$row.addClass('magnet-4k-highlight');
}
}));
hasHighQuality ? $("#magnets-content .magnet-row").not(".high-quality").hide() : $("#enable-magnets-filter").addClass("do-hide");
});
hiddenInfos.forEach(info => $(info.row).hide());
}
showAll() {
if (isJavDb) {
$("#magnets-content .item[data-rank]").toArray().forEach((el => {
$(el).show();
$(el).removeClass('magnet-4k-highlight');
}));
}
if (isJavBus) {
$("#magnet-table tr").toArray().forEach((el => $(el).show()));
}
}
handleBus() {
isJavBus && window.isDetailPage && utils.loopDetector((() => $("#magnet-table td a").length > 0), (() => {
@@ -44,12 +132,6 @@ class HighlightMagnetPlugin extends BasePlugin {
})) : $("#enable-magnets-filter").addClass("do-hide");
}));
}
showAll() {
if (isJavDb) {
$("#magnets-content .item").toArray().forEach((el => $(el).show()));
}
isJavBus && $("#magnet-table tr").toArray().forEach((el => $(el).show()));
}
}
+3 -1
View File
@@ -402,11 +402,13 @@ class ListPagePlugin extends BasePlugin {
}
if (this.cache[carNum2]) {
let _this = this;
$title.attr("data-original-title", content);
$title.contents().each((function() {
3 === this.nodeType && "" !== this.textContent.trim() && (this.textContent = " " + _this.cache[carNum2] + " ");
}));
$title.attr("title", _this.cache[carNum2]);
} else translateText(content).then((result => {
$title.attr("data-original-title", content);
if (isJavDb) {
$title.contents().each((function() {
3 !== this.nodeType || "" === this.textContent.trim() || this.textContent.includes(carNum2) || (this.textContent = " " + result + " ");
@@ -424,7 +426,7 @@ class ListPagePlugin extends BasePlugin {
async revertTranslation() {
$(this.getSelector().itemSelector).toArray().forEach((ele => {
let $box2 = $(ele);
const originalContent = $box2.find(".box").attr("title") || $box2.find(".video-title").attr("title") || $box2.find("img").attr("data-title");
const originalContent = $box2.find(".video-title").attr("data-original-title") || $box2.find(".box").attr("title") || $box2.find(".video-title").attr("title") || $box2.find("img").attr("data-title");
let carNum2;
isJavDb && (carNum2 = $box2.find(".video-title strong").text().trim());
const $title = $box2.find(".video-title");
File diff suppressed because one or more lines are too long
+58 -15
View File
@@ -393,10 +393,13 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
async batchDeleteMarked(selectedItems, onProgress) {
let deletedCount = 0;
let failedCount = 0;
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const randomDelay = () => new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000));
for (let i = 0; i < selectedItems.length; i++) {
const item = selectedItems[i];
try {
if (i > 0) {
await randomDelay();
}
const matchList = await this.searchFiles(item.carNum);
if (matchList.length === 0) {
failedCount++;
@@ -412,24 +415,49 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
}
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++;
for (let d = 0; d < uniqueDirIds.length; d++) {
if (d > 0) {
await randomDelay();
}
const dirId = uniqueDirIds[d];
let retryCount = 0;
const maxRetries = 3;
while (retryCount <= maxRetries) {
try {
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
"fid[0]": dirId,
ignore_warn: "1"
});
if (result && result.state) {
itemDeleted++;
deletedCount++;
break;
} else {
const errMsg = (result && result.error) || "未知错误";
if (retryCount < maxRetries && this.isRateLimited(result)) {
retryCount++;
const backoff = (retryCount + 1) * 2000 + Math.random() * 1000;
onProgress && onProgress(i + 1, selectedItems.length, item, `风控触发,${backoff / 1000}秒后重试(${retryCount}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, backoff));
continue;
}
console.error(`删除目录失败: ${dirId}`, errMsg);
break;
}
} catch (e) {
if (retryCount < maxRetries && this.isRateLimited(e)) {
retryCount++;
const backoff = (retryCount + 1) * 2000 + Math.random() * 1000;
onProgress && onProgress(i + 1, selectedItems.length, item, `风控触发,${backoff / 1000}秒后重试(${retryCount}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, backoff));
continue;
}
console.error(`删除目录失败: ${dirId}`, e);
break;
}
} 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 || "出错");
@@ -439,6 +467,21 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
return {deletedCount, failedCount};
}
isRateLimited(response) {
if (!response) return false;
if (response.state === false) {
const msg = (response.error || response.msg || "").toLowerCase();
if (msg.includes("频繁") || msg.includes("操作太频繁") || msg.includes("稍后再试")
|| msg.includes("rate") || msg.includes("limit") || msg.includes("too many")) {
return true;
}
}
if (response.status === 403 || response.status === 429) {
return true;
}
return false;
}
showDeleteDialog(carNum2, matchList, afterDelete) {
const upperCarNum = carNum2.toUpperCase();
const safeMatchList = matchList.filter(m => m.name.toUpperCase().includes(upperCarNum));