32 Commits

Author SHA1 Message Date
lzyme.dev@gmail.com 6a12b45f86 Merge remote-tracking branch 'origin/main' 2026-09-01 00:53:11 +08:00
lzyme.dev@gmail.com 1e3c4d9718 更新文档 2026-09-01 00:50:56 +08:00
FireInRain 2b8c925fed Merge pull request #2 from anupamme/fix-repo-jhs-enhance-remove-hardcoded-imgur-client-id
fix: the userscript embeds imgur api credentials dir... in...
2026-08-31 21:42:16 +08:00
lzyme.dev@gmail.com a9e5286c53 更新文档 2026-08-31 14:31:53 +08:00
anupamme 7005b528a3 fix: V-001 security vulnerability
Automated security fix generated by OrbisAI Security
2026-08-31 06:14:02 +00:00
lzyme.dev@gmail.com 4026b84fc2 更新版本3.3.10 2026-08-31 14:04:44 +08:00
lzyme.dev@gmail.com 76b608ccc0 调整UI 2026-08-31 14:00:58 +08:00
lzyme.dev@gmail.com 6c9a4743bb 新增验车功能 2026-08-31 13:50:35 +08:00
lzyme.dev@gmail.com ce6cedf164 更新磁力搜索 2026-08-31 13:32:56 +08:00
lzyme.dev@gmail.com 7840ca6389 磁力搜索磁力新增排序 2026-08-31 11:49:58 +08:00
lzyme.dev@gmail.com d03a9d1462 修复磁力搜索里面点击下载触发重复请求 2026-08-31 11:31:20 +08:00
lzyme.dev@gmail.com b7cfc8541c update 2026-08-31 08:38:26 +08:00
lzyme.dev@gmail.com ce59d82ed2 更新文档和版本 2026-08-30 22:18:06 +08:00
lzyme.dev@gmail.com 37cf4e1183 新增进入详情页面对不规则名文件进行重命名 2026-08-30 22:15:38 +08:00
lzyme.dev@gmail.com 8829af0fc3 修复不规则vr作品名字无法匹配 2026-08-30 21:43:56 +08:00
lzyme.dev@gmail.com 9db06fba3b 修复layui悬浮窗错误不加载 2026-08-30 20:33:17 +08:00
lzyme.dev@gmail.com 06b2497ee1 修复115固有jq导致加载冲突 2026-08-30 20:10:07 +08:00
lzyme.dev@gmail.com 50581be5a6 修复在115首页导致jq加载冲突 2026-08-30 19:42:34 +08:00
lzyme.dev@gmail.com e92991c365 移除跳转播放无用的地址 2026-08-30 19:09:07 +08:00
lzyme.dev@gmail.com 03700906e5 修复dev下重复依赖 2026-08-30 18:10:31 +08:00
lzyme.dev@gmail.com e61739bd17 修复删除目录潜在的bug 2026-08-30 17:16:46 +08:00
lzyme.dev@gmail.com c7310ae830 更新文档 2026-08-30 16:00:12 +08:00
lzyme.dev@gmail.com fb5120551c 修复预览视频播放 更新文档 2026-08-30 15:57:27 +08:00
lzyme.dev@gmail.com 42bc3f624a 修复磁力排序刷新失效 2026-08-30 14:30:43 +08:00
lzyme.dev@gmail.com 13abbd03fb 修复javdb 视频预览 2026-08-30 14:11:29 +08:00
lzyme.dev@gmail.com 6dfebb50a3 修复javbus搜索 2026-08-30 13:57:46 +08:00
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
26 changed files with 18653 additions and 11853 deletions
+152 -73
View File
@@ -2,7 +2,7 @@
> Jav-鉴黄师 油猴脚本 — 工程化重构版本
基于 `vite-plugin-monkey` 脚手架,将原 10,000+ 行单体脚本拆分为 49 个可维护的 ES Module 模块,支持本地开发热更新、一键构建。
基于 `vite-plugin-monkey` 脚手架,将原 10,000+ 行单体脚本拆分为 56 个可维护的 ES Module 模块,支持本地开发热更新、一键构建。
---
@@ -23,31 +23,68 @@
---
## 自动部署
本项目使用 GitHub Actions 在推送代码时自动构建并发布到 [GitHub Releases](https://github.com/fireinrain/JHS-enhance/releases)。
```
git push → GitHub Actions 自动构建 → 发布 GitHub Release
↓
https://github.com/fireinrain/JHS-enhance/releases/latest/download/jhs-enhance.user.js
↓
用户点击安装链接 → Tampermonkey 弹出安装提示
```
---
## 功能概览
- **列表页增强**:作品状态标签(屏蔽/收藏/已下载/已观看)、一键筛选已收藏/新作品、演员黑名单过滤、标题关键词过滤
- **详情页增强**:磁力链接高亮匹配、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 查看热播
- 自动翻页
### 详情页增强
- **磁力搜索聚合(MagnetHub)**:多引擎聚合搜索(Sukebei / U9A9 / U3C3 / BTSearch / SoKitty / ØMagnet),Tab
切换引擎,结果表格展示(名称/大小/日期/操作),支持按大小/最新/最旧排序,质量标签自动识别(中字/4K/破解),一键复制磁力链接(含番号提取码),验车功能(Whatslink
API 截图预览、键盘/缩放导航),115 离线下载快捷按钮
- **磁力排序与过滤(JavDB)**:按发布日期/文件大小/文件数量拖拽排序,勾选启用;高清/4K/字幕/无码多条件并集过滤,4K 磁力行高亮显示
- 磁力链接高亮匹配已收藏/已下载状态
- DMM 多画质预览视频
- **多源视频预览**:Javxy 聚合 API 独立按钮(番号/识别码右侧),多源 fallback(JavTrailers / JavDB 等),HLS 支持、播放进度记忆、画质切换
- **标题翻译**:Google 翻译,支持开关多次切换不失效
- **字幕搜索**:合并迅雷 + SubTitleCat 为单一入口,支持 115 网盘直传、详情页预匹配目录、多目录选择上传、长目录名截断展示、字幕预览绿色字体
- **多源预览图**:javfree / projectjav / javstore 三源切换,来源按钮绿色高亮,sessionStorage 缓存,错误提示弹窗内显示
- 演员信息展示、相关清单、评论查看
- 以图识图、第三方站点跳转
### 115 网盘集成
- **番号智能匹配**:双阶段搜索策略 — 第一阶段精准搜索(完整番号 + `type:4`),未命中时第二阶段模糊兜底(尾号 +
正则匹配),支持不规则文件名(如 `MIVR00345-5.mp4`、`ABP001.mp4`)
- **磁力离线下载**:MagnetHub 磁力搜索结果支持一键添加至 115 离线下载
- **不规则文件名自动重命名**
:检测到需第二阶段才能匹配的作品时,自动将文件重命名为标准番号格式(如 `mdvr00317-3.mp4` → `MDVR-317-3.mp4`
),设置面板可开关 + 选择范围(仅VR / 全部)
- 作品目录匹配与详情页预加载
- 多目录选择上传字幕
- 字幕一键直传至匹配目录
- **作品删除增强**:删除整个文件夹(非仅视频),支持标记删除批量操作,删除后字幕目录列表联动;批量删除含风控保护(1~3s
随机延迟 + 触发风控自动重试退避);删除安全检查使用 `codeParse` 正则匹配番号
### 数据管理
- IndexedDB 本地存储
- 云盘备份/恢复(阿里云盘 / WebDAV)
- 跨 Tab 数据同步(BroadcastChannel)
### 自动化任务
- 新作品检测
- 收藏演员同步
- 黑名单自动检测与更新
- 后台定时任务调度
### 其他
- FC2 站点支持(含 123AV 补全)
- **MagnetHub 磁力搜索**:6 引擎聚合(Sukebei/U9A9/U3C3/BTSearch/SoKitty/ØMagnet),质量标签(中字/4K/破解),验车截图预览(Whatslink
API,键盘导航),排序切换,115 离线一键下载
- JavTrailers 站点处理
- 设置面板:快捷键配置、域名配置、缓存清理、标记删除列表、打赏作者(二维码/钱包地址可自定义)
- 浏览历史、想看/已看列表
- 多站点支持:JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers / SubTitleCat
---
@@ -59,63 +96,69 @@ 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 层(4 个模块)
│ │ ├── javdb.js # JavDB API 封装(签名算法、搜索、评论、磁力列表)
│ │ ├── dmm.js # DMM 预览视频获取(多画质、默认画质选择)
│ │ ├── dmm-graphql.js # DMM GraphQL API(备选视频源回退)
│ │ └── javxy.js # Javxy 聚合视频 API(多源解析、签名生成)
│ ├── lib/ # 第三方补丁库
│ │ ├── gm-xhr-parallel.js # Tampermonkey MV3 并行请求修复(redirect: manual)
│ │ └── hls-runtime.js # HLS 视频播放运行时(hls.js 封装、进度记忆、画质切换)
│ └── plugins/ # 插件层(42 个功能插件)
│ ├── list-page.js # 列表页过滤与状态标签渲染
│ ├── list-page-button.js # 列表页功能按钮(待鉴定/已收藏/新作品/黑名单管理)
│ ├── detail-page.js # 详情页数据加载与基础增强
│ ├── detail-page-button.js# 详情页操作按钮(115 目录预加载 + 磁力排序过滤面板)
│ ├── setting.js # 设置面板(Tab 页:基础/任务/域名/快捷键/屏蔽/缓存/标记删除/打赏作者;含 115 自动重命名开关 + 范围选择)
│ ├── 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 网盘匹配/标签/删除(含 codeParse 正则匹配、双阶段搜索、自动重命名)
│ ├── 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)
│ ├── magnet-hub.js # MagnetHub 磁力搜索
│ ├── other-site.js # 第三方站点链接
│ ├── jav-trailers.js # JavTrailers 站点
│ ├── subtitle-cat.js # SubTitleCat 字幕搜索(已整合到 subtitles.js,保留文件)
│ ├── image-recognition.js # 以图识图(第三方识图接口)
│ ├── screenshot.js # 多源预览图(javfree/projectjav/javstore + 来源切换弹窗)
│ ├── subtitles.js # 字幕搜索(整合迅雷+SubTitleCat)+ 115 多目录直传
│ ├── req115.js # 115 网盘 API 封装(Req 基类 + Drive115 + Req115)
│ ├── magnet-hub.js # MagnetHub 磁力搜索(6 引擎聚合、质量标签、验车截图、115 离线)
│ ├── other-site.js # 第三方站点跳转链接
│ ├── jav-trailers.js # JavTrailers 站点处理
│ ├── javxy-preview-video.js# Javxy 多源视频预览(独立按钮、聚合 API、HLS 支持)
│ ├── 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 +288,12 @@ window.show.ok('操作成功'); // Toast 通知
---
## 赞助
如果当前项目有帮到您的话,赞助我喝瓶水/Star项目也是极好的,感谢支持 :)
![赞助](https://i.imgur.com/ynGK6he.jpeg)
## 构建配置
`vite-plugin-monkey` 在 `vite.config.mjs` 中配置了:
@@ -258,7 +307,37 @@ window.show.ok('操作成功'); // Toast 通知
## 版本历史
- **v3.3.6** — 工程化重构版本,模块化拆分,支持 HMR 开发
- **v3.3.9**
- 新增:115 番号智能匹配 — `codeParse` 正则匹配引擎,双阶段搜索策略(精准 →
模糊兜底),支持不规则文件名(如 `MIVR00345-5.mp4`、`ABP001.mp4`)
- 新增:115 不规则文件名自动重命名 —
检测到需模糊匹配的作品时自动批量重命名为标准格式(如 `mdvr00317-3.mp4` → `MDVR-317-3.mp4`),设置面板可开关 +
范围选择(仅VR / 全部)
- 优化:删除安全检查改用 `codeParse` 正则匹配,消除 `includes` 方式对不规则文件名的误判
- 优化:`searchFiles` 增加 `type:4` 参数,搜索仅返回视频文件,避免目录占用 30 条槽位
- **v3.3.8**
- 新增:Javxy 多源视频预览独立按钮(番号/识别码右侧),基于 Javxy 聚合 API,支持 JavTrailers / JavDB 等多源回退
- 新增:HLS 视频播放运行时(hls.js 封装),支持 .m3u8 格式、播放进度记忆、画质切换
- 新增:DMM GraphQL API 模块,作为备选视频源回退方案
- 优化:`bus-preview-video.js` 和 `preview-video.js` 清理移植代码,保持原有播放逻辑不变
- 优化:`dmm.js` 回退至仅 DMM Affiliate API + GraphQL API 双通道,移除 Javxy 耦合
- **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 开发热更新
---
+15410 -11409
View File
File diff suppressed because one or more lines are too long
+5 -3
View File
@@ -1,17 +1,16 @@
{
"name": "jhs-enhance",
"version": "3.3.6",
"version": "3.3.10",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "jhs-enhance",
"version": "3.3.6",
"version": "3.3.10",
"license": "MIT",
"dependencies": {
"blueimp-md5": "^2.19.0",
"jquery": "^4.0.0",
"layui-layer": "^1.0.9",
"localforage": "^1.10.0",
"qrcodejs": "^1.0.0",
"tabulator-tables": "^6.5.2",
@@ -19,6 +18,7 @@
"viewerjs": "^1.12.0"
},
"devDependencies": {
"layui-layer": "^1.0.9",
"vite": "^6.0.0",
"vite-plugin-monkey": "^5.0.0"
}
@@ -1204,6 +1204,7 @@
"version": "1.0.9",
"resolved": "https://registry.npmmirror.com/layui-layer/-/layui-layer-1.0.9.tgz",
"integrity": "sha512-CzPVtG0D4IvmISUyLTzzbz2Uxfaoqch+87lHqVmO53ie8ZKOfOkENbBl7ssiLna1lAnv4M1SycmH62NbtndtDQ==",
"dev": true,
"license": "ISC",
"dependencies": {
"jquery": "^3.1.1"
@@ -1213,6 +1214,7 @@
"version": "3.7.1",
"resolved": "https://registry.npmmirror.com/jquery/-/jquery-3.7.1.tgz",
"integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==",
"dev": true,
"license": "MIT"
},
"node_modules/lie": {
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "jhs-enhance",
"version": "3.3.6",
"version": "3.3.10",
"description": "Jav-鉴黄师 油猴脚本 - 工程化版本",
"type": "module",
"scripts": {
+190
View File
@@ -0,0 +1,190 @@
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
const DMM_GRAPHQL_URL = 'https://api.video.dmm.co.jp/graphql';
const dmmGraphQLKeyword = (code) => {
const match = String(code || '').trim().toUpperCase().match(/^([A-Z0-9]{2,10})-(\d{1,6})$/);
if (!match) return '';
return `${match[1].toLowerCase()}${match[2].padStart(5, '0')}`;
};
const dmmGraphQLRequest = (query) => {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: 'POST',
url: DMM_GRAPHQL_URL,
data: JSON.stringify({query}),
timeout: 12000,
headers: {
Accept: 'application/json',
'Content-Type': 'application/json',
Referer: 'https://video.dmm.co.jp/',
'Fanza-Device': 'BROWSER',
'Cache-Control': 'no-cache',
},
onload: (response) => {
if (response.status < 200 || response.status >= 400) {
reject(new Error(`DMM GraphQL HTTP ${response.status || 0}`));
return;
}
try {
const json = JSON.parse(response.responseText || '{}');
if (Array.isArray(json.errors) && json.errors.length) {
reject(new Error(json.errors[0].message || 'DMM GraphQL error'));
return;
}
resolve(json.data || {});
} catch (e) {
reject(new Error(`DMM GraphQL JSON parse error: ${e.message}`));
}
},
onerror: () => reject(new Error('DMM GraphQL network error')),
ontimeout: () => reject(new Error('DMM GraphQL timeout')),
});
});
};
const loadDmmCover = async (code) => {
const keyword = dmmGraphQLKeyword(code);
if (!keyword) throw new Error('DMM cover keyword missing');
const searchKeywords = [...new Set([keyword, `${keyword}#`])];
for (const searchKeyword of searchKeywords) {
const query = `{ legacySearchPPV(limit: 5, offset: 0, sort: SALES_RANK_SCORE, floor: AV, queryWord: ${JSON.stringify(searchKeyword)}) { result { contents { id } } } }`;
const data = await dmmGraphQLRequest(query);
const ids = Array.isArray(data && data.legacySearchPPV && data.legacySearchPPV.result && data.legacySearchPPV.result.contents)
? data.legacySearchPPV.result.contents.map((item) => String(item && item.id || '').trim()).filter(Boolean)
: [];
const compactKeyword = keyword.replace(/[^a-z0-9]/gi, '').toLowerCase();
const contentID =
ids.find((id) => id.replace(/[^a-z0-9]/gi, '').toLowerCase().includes(compactKeyword)) ||
(ids.length === 1 ? ids[0] : '');
if (contentID) {
const encoded = encodeURIComponent(contentID);
return `https://pics.dmm.co.jp/digital/video/${encoded}/${encoded}pl.jpg`;
}
}
throw new Error('DMM cover missing');
};
const searchDmmContentIdsByGraphQL = async (code) => {
const keyword = dmmGraphQLKeyword(code);
if (!keyword) return [];
const carNumNoHyphen = String(code || '').replace(/-/g, '').toLowerCase();
const results = [];
const searchKeywords = [...new Set([keyword, `${keyword}#`])];
for (const searchKeyword of searchKeywords) {
const query = `{ legacySearchPPV(limit: 10, offset: 0, sort: SALES_RANK_SCORE, floor: AV, queryWord: ${JSON.stringify(searchKeyword)}) { result { contents { contentId serviceCode floorCode } } } }`;
const data = await dmmGraphQLRequest(query);
const contents = Array.isArray(data && data.legacySearchPPV && data.legacySearchPPV.result && data.legacySearchPPV.result.contents)
? data.legacySearchPPV.result.contents
: [];
for (const item of contents) {
const contentId = String(item.contentId || '');
if (contentId.replace(/[^a-z0-9]/gi, '').toLowerCase().includes(carNumNoHyphen)) {
results.push({
serviceCode: item.serviceCode || 'digital',
floorCode: item.floorCode || 'AV',
contentId: contentId,
});
}
}
if (results.length > 0) break;
}
return results;
};
const getDmmGraphQLVideoUrls = async (code) => {
const contentItems = await searchDmmContentIdsByGraphQL(code);
if (!contentItems.length) return null;
const results = await Promise.allSettled(
contentItems.map((item) => extractTrailerLinks(item))
);
for (const result of results) {
if (result.status === 'fulfilled' && result.value) {
return result.value;
}
}
return null;
};
const extractTrailerLinks = (item) => {
const {contentId, serviceCode, floorCode} = item;
return new Promise((resolve, reject) => {
const trailerPageUrl = `https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=${contentId}/mtype=AhRVShI_/service=${serviceCode}/floor=${floorCode}/mode=/`;
GM_xmlhttpRequest({
method: 'GET',
url: trailerPageUrl,
timeout: 15000,
headers: {
'accept-language': 'ja-JP,ja;q=0.9',
Cookie: 'age_check_done=1',
},
onload: (response) => {
const htmlContent = response.responseText || '';
if (htmlContent.includes('このサービスはお住まいの地域からは')) {
reject(new Error('节点不可用,请将DMM域名分流到日本ip'));
return;
}
const match = htmlContent.match(/const\s+args\s+=\s+(.*);/);
if (!match) {
reject(new Error('未在脚本中找到 const args = ... 变量'));
return;
}
let bitrates;
try {
bitrates = JSON.parse(match[1]).bitrates;
} catch (e) {
reject(new Error(`解析播放器脚本 JSON 失败: ${e.message}`));
return;
}
if (!Array.isArray(bitrates)) {
reject(new Error('解析画质链接失败: bitrates 字段不是一个数组或不存在'));
return;
}
const qualityKeys = [
'4ks', '4k', 'hhbs', 'hhb', 'hmb', 'mhb', 'mmb',
'dmb_w', 'dmb_s', 'dm_s', 'sm_s', 'mhb_w',
];
const qualityNameRegex = new RegExp(`(${qualityKeys.join('|')})\\.mp4$`);
const finalQualityMap = {};
for (const bitrate of bitrates) {
const url = bitrate && bitrate.src;
if (!url || typeof url !== 'string' || !url.endsWith('.mp4')) continue;
const qualityMatch = url.match(qualityNameRegex);
let qualityKey = '';
if (qualityMatch && qualityMatch[1]) qualityKey = qualityMatch[1];
if (qualityKey && !finalQualityMap[qualityKey]) {
finalQualityMap[qualityKey] = url;
}
}
if (Object.keys(finalQualityMap).length === 0) {
reject(new Error('未找到匹配要求的预览画质视频'));
return;
}
resolve(finalQualityMap);
},
onerror: () => reject(new Error('DMM 播放页请求网络错误')),
ontimeout: () => reject(new Error('DMM 播放页请求超时')),
});
});
};
export {
DMM_GRAPHQL_URL,
dmmGraphQLKeyword,
dmmGraphQLRequest,
loadDmmCover,
searchDmmContentIdsByGraphQL,
getDmmGraphQLVideoUrls,
extractTrailerLinks,
};
+31 -3
View File
@@ -1,4 +1,6 @@
import { qualityOptions } from '../core/constants.js';
import {getDmmGraphQLVideoUrls} from './dmm-graphql.js';
const selectDefaultQuality = (dmmVideoQualityList, intendedDefault) => {
if (!dmmVideoQualityList || 0 === dmmVideoQualityList.length) return null;
const availableSet = new Set(dmmVideoQualityList);
@@ -138,6 +140,25 @@ class DmmVideoFetcher {
if (0 === Object.keys(finalQualityMap).length) throw new Error("未找到匹配要求的预览画质视频");
return finalQualityMap;
}
async _searchViaGraphQL() {
clog.debug("尝试通过 GraphQL API 搜索视频...");
const videoMap = await getDmmGraphQLVideoUrls(this.carNum);
if (videoMap && Object.keys(videoMap).length > 0) {
this._updateCache(videoMap);
clog.debug("GraphQL 成功解析出预览视频:", videoMap);
const dmmCacheKey = "jhs_other_site_dmm";
const dmmCacheData = localStorage.getItem(dmmCacheKey) ? JSON.parse(localStorage.getItem(dmmCacheKey)) : {};
dmmCacheData[this.carNum] = {
type: "graphql",
url: `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`,
};
localStorage.setItem(dmmCacheKey, JSON.stringify(dmmCacheData));
return videoMap;
}
return null;
}
async fetchVideo() {
const cachedResult = this._checkCache();
if (cachedResult) return cachedResult;
@@ -149,22 +170,29 @@ class DmmVideoFetcher {
contentItems = await this._searchContentIds();
} catch (e) {
clog.error("DMM API 搜索失败:", e);
}
if (!contentItems || 0 === contentItems.length) {
clog.warn("Affiliate API 未找到视频,尝试 GraphQL API...");
const graphQLResult = await this._searchViaGraphQL();
if (graphQLResult) return graphQLResult;
const $btn = $("#fanzaBtn");
$btn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`);
$btn.attr("title", "未查询到, 点击前往搜索页");
$btn.css("backgroundColor", "#de3333");
return null;
}
if (!contentItems || 0 === contentItems.length) return null;
try {
const finalVideoMap = await Promise.any(contentItems.map((item => this._extractTrailerLinks(item))));
this._updateCache(finalVideoMap);
return finalVideoMap;
} catch (error) {
const errors = error.errors || [ error ];
clog.error(`Affiliate 解析失败: ${errors[0] && errors[0].message || errors[0]}`, errors);
clog.warn("尝试 GraphQL API 作为 fallback...");
const graphQLResult = await this._searchViaGraphQL();
if (graphQLResult) return graphQLResult;
if (errors.some((err => err.message.includes("节点不可用")))) this.showErrorMessages && show.error("节点不可用,请将DMM域名分流到日本ip"); else {
const displayError = errors[0].message || errors[0];
clog.error(`解析失败: ${displayError}`, errors);
this.showErrorMessages && show.error(`解析失败: ${displayError}`);
}
const $btn = $("#fanzaBtn");
@@ -178,4 +206,4 @@ class DmmVideoFetcher {
const getDmmVideo = async (carNum2, showErrorMessages = !0) => new DmmVideoFetcher(carNum2, showErrorMessages).fetchVideo();
export { DmmVideoFetcher, getDmmVideo, selectDefaultQuality };
export { DmmVideoFetcher, getDmmVideo, selectDefaultQuality };
+223
View File
@@ -0,0 +1,223 @@
import {qualityOptions} from '../core/constants.js';
const JAVXY_TOKEN = [118, 119, 112, 71, 97, 110, 28, 84, 124, 65, 76, 102, 65, 16, 77, 109, 64, 82, 85, 83, 67, 92, 125, 108, 83, 65, 124, 107, 84, 104, 71, 84, 17, 124, 118, 125, 104, 8, 125, 96, 112, 103, 29, 18, 82, 83, 87, 84]
.map(v => String.fromCharCode(v ^ 0x25)).join('');
const JAVXY_ENDPOINTS = [
{host: String.fromCharCode(106, 97, 118, 120, 121, 46, 99, 99, 46, 99, 100), label: 'Javxy'},
{
host: String.fromCharCode(119, 111, 114, 107, 101, 114, 46, 106, 97, 118, 120, 121, 46, 99, 99, 46, 99, 100),
label: 'Javxy Worker'
},
];
const JAVXY_SOURCE_LABELS = {
'Tokyo-Hot': 'Javxy | Tokyo-Hot',
FC2: 'Javxy | FC2',
Direct: 'Javxy | Direct',
DMM: 'Javxy | dmm',
MGStage: 'Javxy | MGStage',
DUGA: 'Javxy | DUGA',
MYWIFE: 'Javxy | MyWife',
JavTrailers: 'Javxy | JavTrailers',
JavDB: 'Javxy | Javdb',
AVWikiDB: 'Javxy | AVWikiDB',
JAVDatabase: 'Javxy | JAVDatabase',
HEYZO: 'Javxy | Heyzo',
HeyDouga: 'Javxy | HeyDouga',
PACO: 'Javxy | Paco',
'10MU': 'Javxy | 10mu',
Caribbean: 'Javxy | 加勒比',
'1Pondo': 'Javxy | 一本道',
};
const JAVXY_QUALITY_OPTIONS = [
{quality: '4k', text: '4K'},
{quality: 'hhb', text: '1080p'},
{quality: '1080p', text: '1080p'},
{quality: 'hmb', text: '720p'},
{quality: '720p', text: '720p'},
{quality: 'mhb', text: '576p'},
{quality: '540p', text: '540p'},
{quality: 'mmb', text: '432p'},
{quality: '480p', text: '480p'},
{quality: '396p', text: '396p'},
{quality: '360p', text: '360p'},
{quality: '240p', text: '240p'},
];
const jpSourceFailedKey = (source = 'DMM') => `javxy_jp_source_failed_until_${String(source || '').toUpperCase()}`;
const isJpSourceTemporarilyFailed = (source = 'DMM') => {
const until = Number(sessionStorage.getItem(jpSourceFailedKey(source)) || 0);
if (!Number.isFinite(until) || until <= Date.now()) {
sessionStorage.removeItem(jpSourceFailedKey(source));
return false;
}
return true;
};
const markJpSourceTemporarilyFailed = (source = 'DMM') => {
sessionStorage.setItem(jpSourceFailedKey(source), String(Date.now() + 30 * 60 * 1000));
};
const selectHighestQuality = (qualityMap) => {
const rank = new Map(JAVXY_QUALITY_OPTIONS.map((item, index) => [item.quality, index]));
return Object.keys(qualityMap || {})
.filter(key => qualityMap[key])
.sort((a, b) => (rank.get(a) ?? -1) - (rank.get(b) ?? -1))[0] || null;
};
const sortQualityKeys = (qualityMap) => {
const rank = new Map(JAVXY_QUALITY_OPTIONS.map((item, index) => [item.quality, index]));
return Object.keys(qualityMap || {})
.filter(key => qualityMap[key])
.sort((a, b) => (rank.get(a) ?? -1) - (rank.get(b) ?? -1));
};
const normalizeJavxySource = (value) => {
const raw = String(value || '').trim().toLowerCase();
if (raw.includes('fc2')) return 'FC2';
if (raw.includes('mgstage')) return 'MGStage';
if (raw.includes('heydouga')) return 'Direct';
if (raw.includes('mywife')) return 'MYWIFE';
if (raw.includes('duga')) return 'DUGA';
if (raw.includes('javtrailers')) return 'JavTrailers';
if (raw.includes('javdb')) return 'JavDB';
if (raw.includes('avwikidb')) return 'AVWikiDB';
if (raw.includes('javdatabase')) return 'JAVDatabase';
if (raw.includes('dmm')) return 'DMM';
if (raw === 'direct' || raw.includes('heyzo') || raw.includes('heydouga') || raw.includes('paco') || raw.includes('10musume') || raw.includes('10mu') || raw.includes('1pondo') || raw.includes('caribbean') || raw.includes('tokyo-hot') || raw.includes('tokyohot')) return 'Direct';
return String(value || '').trim();
};
const javxyQualityMapToDmmFormat = (javxyQualityMap) => {
const result = {};
const qualityKeys = qualityOptions.map(o => o.quality);
for (const [key, url] of Object.entries(javxyQualityMap || {})) {
if (!url) continue;
const lowerKey = key.toLowerCase();
if (qualityKeys.includes(key)) {
result[key] = url;
} else if (qualityKeys.includes(lowerKey)) {
result[lowerKey] = url;
}
}
return result;
};
const fromJavxyCcCd = async (id, rawCode = '', options = {}) => {
const query = String(id || rawCode || '').trim();
if (!query) {
clog.debug('Javxy 跳过:查询词为空');
return null;
}
for (const endpoint of JAVXY_ENDPOINTS) {
const params = new URLSearchParams({client: 'laosiji-new'});
if (Array.isArray(options.skip) && options.skip.length) params.set('skip', options.skip.join(','));
if (Array.isArray(options.prefer) && options.prefer.length) params.set('prefer', options.prefer.join(','));
if (Array.isArray(options.source) && options.source.length) params.set('source', options.source.join(','));
if (options.playbackFallback) params.set('purpose', 'playback-fallback');
const apiUrl = `https://${endpoint.host}/trailers/${encodeURIComponent(query)}?${params}`;
clog.debug('Javxy 请求 API', {query, apiUrl, endpoint: endpoint.label});
let r;
try {
r = await gmHttp.get(apiUrl, null, {
'Accept': 'application/json,text/plain,*/*',
'X-Javxy-Token': JAVXY_TOKEN,
}, {timeout: 8000});
} catch (e) {
clog.debug('Javxy API 网络失败,尝试下一个节点', {endpoint: endpoint.label, error: e.message});
continue;
}
if (!r) {
clog.debug('Javxy API 无响应,尝试下一个节点', {endpoint: endpoint.label});
continue;
}
const trailerUrl = String(r?.trailer || '').trim();
if (!trailerUrl) {
clog.debug('Javxy 无 trailer 字段', {endpoint: endpoint.label, keys: Object.keys(r || {})});
return null;
}
const qualityMap = r?.qualities && typeof r.qualities === 'object' ? r.qualities : {};
const quality = r?.quality && qualityMap[r.quality] ? r.quality : selectHighestQuality(qualityMap);
const sourceBase = JAVXY_SOURCE_LABELS[r?.source] || `Javxy | ${r?.source || 'dmm'}`;
const directUrl = qualityMap[quality] || trailerUrl;
clog.debug('Javxy 返回结果', {
endpoint: endpoint.label,
source: r?.source,
quality,
qualities: Object.keys(qualityMap),
url: directUrl,
});
const dmmQualityMap = javxyQualityMapToDmmFormat(qualityMap);
return {
url: directUrl,
source: sourceBase,
type: String(r?.type || '').trim() || 'video',
qualities: qualityMap,
quality,
directUrl,
code: id,
rawCode,
javxySource: String(r?.source || '').trim(),
requiresJP: Boolean(r?.requiresJP),
urls: Array.isArray(r?.urls) && r.urls.length ? r.urls : sortQualityKeys(qualityMap).map(key => qualityMap[key]),
dmmQualityMap,
};
}
return null;
};
const fallbackJavxyResult = async (code, rawCode = '', failedSources = [], options = {}) => {
const skip = [...new Set((failedSources || []).map(source => normalizeJavxySource(source)).filter(Boolean))];
const source = [...new Set((options.source || []).map(s => String(s || '').trim()).filter(Boolean))];
if (!skip.length && !source.length) return null;
clog.debug('Javxy 播放失败回落查询', {code, skip, source});
return fromJavxyCcCd(code, rawCode, {skip, source, playbackFallback: true});
};
const getJavxyVideoUrls = async (code, failedSources = []) => {
const result = await fallbackJavxyResult(code, code, failedSources, {
source: ['JavTrailers', 'JavDB'],
});
if (result?.dmmQualityMap && Object.keys(result.dmmQualityMap).length > 0) {
return result.dmmQualityMap;
}
if (result?.url) {
const qualityMap = {};
qualityMap['720p'] = result.url;
return qualityMap;
}
return null;
};
export {
fromJavxyCcCd,
fallbackJavxyResult,
getJavxyVideoUrls,
normalizeJavxySource,
isJpSourceTemporarilyFailed,
markJpSourceTemporarilyFailed,
JAVXY_TOKEN,
JAVXY_ENDPOINTS,
};
+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>
`;
+9 -3
View File
@@ -7,6 +7,14 @@ 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';
window.$ = window.jQuery = $;
if (typeof $.trim !== 'function') {
$.trim = function (text) {
return text == null ? '' : (text + '').trim();
};
}
if (typeof jQuery !== 'undefined' && typeof layer === 'undefined') {
await import('layui-layer');
}
window.localforage = localforage;
window.Toastify = Toastify;
window.md5 = md5;
@@ -26,6 +34,4 @@ window.Status_FAVORITE = Status_FAVORITE;
window.Status_HAS_DOWN = Status_HAS_DOWN;
window.Status_HAS_WATCH = Status_HAS_WATCH;
window.NO = NO;
window.YES = YES;
import 'layui-layer';
window.YES = YES;
+1 -1
View File
@@ -98,7 +98,7 @@ class Utils {
}
if (!viewerExists) {
this.layerIndexStack.pop();
layer.close(topLayerIndex);
typeof layer !== 'undefined' && layer.close(topLayerIndex);
}
}
setupEscClose(layerIndex) {
+266
View File
@@ -0,0 +1,266 @@
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
const HLS_SCRIPT_URL = 'https://cdn.jsdelivr.net/npm/hls.js@1.5.18/dist/hls.min.js';
let hlsLoadPromise = null;
const getHlsClass = () => {
const HlsClass = window.Hls || globalThis.Hls || (typeof Hls !== 'undefined' ? Hls : null);
return HlsClass && HlsClass.isSupported && HlsClass.isSupported() ? HlsClass : null;
};
const binaryTextToArrayBuffer = (value) => {
const text = String(value || '');
const bytes = new Uint8Array(text.length);
for (let index = 0; index < text.length; index += 1) {
bytes[index] = text.charCodeAt(index) & 0xff;
}
return bytes.buffer;
};
const loadHlsLibrary = () => {
const readyHls = getHlsClass();
if (readyHls) return Promise.resolve(readyHls);
if (hlsLoadPromise) return hlsLoadPromise;
const loadByGm = () => new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: HLS_SCRIPT_URL,
timeout: 15000,
onload: (r) => {
if (r.status >= 200 && r.status < 300 && r.responseText) {
try {
Function(`${r.responseText}\n//# sourceURL=${HLS_SCRIPT_URL}`).call(globalThis);
} catch (err) {
clog.error('HLS: hls.js 执行失败', err);
}
}
resolve(getHlsClass());
},
onerror: () => resolve(getHlsClass()),
ontimeout: () => resolve(getHlsClass()),
});
});
hlsLoadPromise = new Promise((resolve) => {
const existing = document.querySelector('script[data-jhs-hls="1"]');
if (existing) {
existing.addEventListener('load', () => resolve(getHlsClass()), {once: true});
existing.addEventListener('error', () => loadByGm().then(resolve), {once: true});
setTimeout(() => {
if (!getHlsClass()) loadByGm().then(resolve);
}, 4000);
return;
}
const hlsScript = document.createElement('script');
hlsScript.src = HLS_SCRIPT_URL;
hlsScript.async = true;
hlsScript.dataset.jhsHls = '1';
hlsScript.onload = () => resolve(getHlsClass());
hlsScript.onerror = () => loadByGm().then(resolve);
document.head.appendChild(hlsScript);
}).then((HlsClass) => {
if (!HlsClass) hlsLoadPromise = null;
return HlsClass;
});
return hlsLoadPromise;
};
const createHlsLoader = () => {
class GMHlsLoader {
constructor(config) {
this.config = config;
this.context = null;
this.callbacks = null;
this.loader = null;
this.stats = this.createStats();
}
createStats() {
return {
aborted: false,
loaded: 0,
retry: 0,
total: 0,
chunkCount: 0,
bwEstimate: 0,
trequest: 0,
tfirst: 0,
tload: 0,
loading: {start: 0, first: 0, end: 0},
parsing: {start: 0, end: 0},
buffering: {start: 0, first: 0, end: 0},
};
}
destroy() {
this.abort();
}
abort() {
if (this.stats) this.stats.aborted = true;
if (this.loader) {
try {
this.loader.abort();
} catch (e) { /* ignore */
}
}
this.loader = null;
}
load(context, config, callbacks) {
this.context = context;
this.callbacks = callbacks;
const requestUrl = context.url;
const wantsArrayBuffer =
context.responseType === 'arraybuffer' ||
/\.(?:ts|m4s|mp4|key)(?:[?#]|$)/i.test(requestUrl);
const startedAt = performance.now();
const stats = (this.stats = this.createStats());
stats.trequest = startedAt;
stats.tfirst = startedAt;
stats.tload = startedAt;
stats.loading.start = startedAt;
this.loader = GM_xmlhttpRequest({
method: 'GET',
url: requestUrl,
responseType: 'text',
overrideMimeType: wantsArrayBuffer ? 'text/plain; charset=x-user-defined' : undefined,
timeout: config && config.timeout ? config.timeout : 20000,
headers: {
Accept: wantsArrayBuffer
? '*/*'
: 'application/vnd.apple.mpegurl, application/x-mpegURL, */*',
},
onprogress: (event) => {
stats.loaded = Number(event && event.loaded ? event.loaded : stats.loaded || 0);
stats.total = Number(event && event.total ? event.total : stats.total || stats.loaded || 0);
if (!stats.loading.first && stats.loaded > 0) {
stats.loading.first = performance.now();
}
},
onload: (r) => {
const status = Number(r.status || 0);
const response = {
code: status,
text: r.statusText || '',
url: r.finalUrl || requestUrl,
};
stats.tfirst = stats.tfirst || performance.now();
stats.tload = performance.now();
stats.loading.first = stats.loading.first || stats.tload;
stats.loading.end = stats.tload;
if (status < 200 || status >= 300) {
callbacks.onError && callbacks.onError(response, context, null, stats);
return;
}
const responseText = r.responseText || r.response || '';
const data = wantsArrayBuffer
? binaryTextToArrayBuffer(responseText)
: responseText;
stats.loaded = data && data.byteLength ? data.byteLength : (data && data.length ? data.length : stats.loaded || 0);
stats.total = stats.total || stats.loaded;
stats.bwEstimate =
stats.loading.end > stats.loading.first
? Math.round((stats.total * 8000) / (stats.loading.end - stats.loading.first))
: 0;
callbacks.onSuccess && callbacks.onSuccess({data, url: response.url}, stats, context, response);
},
onerror: () => {
callbacks.onError &&
callbacks.onError({code: 0, text: 'network error', url: requestUrl}, context, null, stats);
},
ontimeout: () => {
stats.tload = performance.now();
stats.loading.end = stats.tload;
callbacks.onTimeout && callbacks.onTimeout(stats, context, null);
},
});
}
}
return GMHlsLoader;
};
const isM3U8Url = (url) => /\.m3u8(?:[?#].*)?$/i.test(String(url || ''));
const attachHlsToVideo = (videoEl, src) => {
return new Promise((resolve, reject) => {
if (!videoEl || !src) {
reject(new Error('video element or source missing'));
return;
}
const HlsClass = getHlsClass();
if (!HlsClass) {
videoEl.src = src;
videoEl.load && videoEl.load();
resolve(false);
return;
}
const hls = new HlsClass({
enableWorker: false,
lowLatencyMode: true,
loader: createHlsLoader(),
autoStartLoad: true,
startPosition: 0,
capLevelToPlayerSize: true,
testBandwidth: false,
preferManagedMediaSource: false,
maxBufferLength: 6,
maxMaxBufferLength: 12,
backBufferLength: 30,
maxBufferHole: 0.5,
nudgeOffset: 0.1,
manifestLoadingMaxRetry: 2,
levelLoadingMaxRetry: 2,
fragLoadingMaxRetry: 2,
manifestLoadingTimeOut: 12000,
levelLoadingTimeOut: 12000,
fragLoadingTimeOut: 12000,
abrEwmaFastLive: 3,
abrEwmaSlowLive: 9,
});
hls.on(HlsClass.Events.MANIFEST_PARSED, () => {
hls.startLoad(0);
videoEl.play().catch(() => {
});
resolve(true);
});
hls.on(HlsClass.Events.ERROR, (_, data) => {
if (!data || !data.fatal) return;
clog.error('HLS: 播放失败', data);
if (data.type === HlsClass.ErrorTypes.MEDIA_ERROR) {
if (videoEl.readyState >= 2) {
try {
hls.recoverMediaError && hls.recoverMediaError();
} catch (e) { /* ignore */
}
return;
}
}
reject(new Error(data.details || 'HLS fatal error'));
});
hls.loadSource(src);
hls.attachMedia(videoEl);
videoEl._hls = hls;
});
};
const destroyHls = (videoEl) => {
if (videoEl && videoEl._hls) {
try {
videoEl._hls.destroy();
} catch (e) { /* ignore */
}
videoEl._hls = null;
}
};
export {loadHlsLibrary, getHlsClass, createHlsLoader, isM3U8Url, attachHlsToVideo, destroyHls, HLS_SCRIPT_URL};
+26 -21
View File
@@ -29,6 +29,7 @@ import { TOP250Plugin } from './plugins/top250.js';
import { NavBarPlugin } from './plugins/nav-bar.js';
import { BusDetailPagePlugin } from './plugins/bus-detail-page.js';
import { BusPreviewVideoPlugin } from './plugins/bus-preview-video.js';
import {JavxyPreviewVideoPlugin} from './plugins/javxy-preview-video.js';
import { ImageRecognitionPlugin } from './plugins/image-recognition.js';
import { BusNavBarPlugin } from './plugins/bus-nav-bar.js';
import { RelatedPlugin } from './plugins/related.js';
@@ -819,29 +820,31 @@ window.ImageHoverPreview = class {
}));
}();
const originalLayerClose = layer.close;
if (typeof layer !== 'undefined') {
const originalLayerClose = layer.close;
layer.close = function(index) {
const result = originalLayerClose.call(this, index);
!function(waitTime = 10) {
setTimeout((() => {
const openLayerCount = document.querySelectorAll(".layui-layer-shade").length;
document.documentElement.style.overflow = openLayerCount > 0 ? "hidden" : "";
}), waitTime);
}();
return result;
};
const originalLayerOpen = layer.open;
layer.open = function(options) {
const originalSuccess = (options = options || {}).success;
options.success = function(layero, index) {
"function" == typeof originalSuccess && originalSuccess.call(this, layero, index);
utils.setupEscClose(index);
layer.close = function (index) {
const result = originalLayerClose.call(this, index);
!function (waitTime = 10) {
setTimeout((() => {
const openLayerCount = document.querySelectorAll(".layui-layer-shade").length;
document.documentElement.style.overflow = openLayerCount > 0 ? "hidden" : "";
}), waitTime);
}();
return result;
};
return originalLayerOpen.call(this, options);
};
const originalLayerOpen = layer.open;
layer.open = function (options) {
const originalSuccess = (options = options || {}).success;
options.success = function (layero, index) {
"function" == typeof originalSuccess && originalSuccess.call(this, layero, index);
utils.setupEscClose(index);
};
return originalLayerOpen.call(this, options);
};
}
utils.importResource("https://cdn.jsdelivr.net/npm/layui-layer@1.0.9/layer.min.css");
@@ -876,6 +879,7 @@ const pluginManager = function() {
pluginManager2.register(DetailPageButtonPlugin);
pluginManager2.register(HighlightMagnetPlugin);
pluginManager2.register(PreviewVideoPlugin);
pluginManager2.register(JavxyPreviewVideoPlugin);
pluginManager2.register(FilterTitleKeywordPlugin);
pluginManager2.register(ActressInfoPlugin);
pluginManager2.register(OtherSitePlugin);
@@ -906,6 +910,7 @@ const pluginManager = function() {
pluginManager2.register(FilterTitleKeywordPlugin);
pluginManager2.register(HighlightMagnetPlugin);
pluginManager2.register(BusPreviewVideoPlugin);
pluginManager2.register(JavxyPreviewVideoPlugin);
pluginManager2.register(MagnetHubPlugin);
pluginManager2.register(ScreenShotPlugin);
pluginManager2.register(OtherSitePlugin);
+17 -7
View File
@@ -1,10 +1,12 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, qualityOptions } from '../core/constants.js';
import {getDmmVideo, selectDefaultQuality} from '../api/dmm.js';
class BusPreviewVideoPlugin extends BasePlugin {
getName() {
return "BusPreviewVideoPlugin";
}
async initCss() {
return "\n /* 弹窗/Modal 样式 */\n .bus-preview-modal {\n position: fixed;\n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n background-color: rgba(0, 0, 0, 0.95); \n /* 关键修改:更新 z-index */\n z-index: 12345699; \n display: flex;\n justify-content: center;\n align-items: center;\n opacity: 0; \n visibility: hidden; \n transition: opacity 0.2s ease;\n }\n .bus-preview-modal.is-open {\n opacity: 1;\n visibility: visible;\n }\n /* 垂直排列视频和按钮,并居中 */\n .bus-preview-modal-content {\n position: relative;\n max-width: 95%; \n max-height: 95%;\n display: flex; \n flex-direction: column; \n align-items: center; \n gap: 15px; \n }\n \n /* 移除 .bus-preview-close-btn 的样式 */\n\n /* 视频播放器容器 */\n .video-player-wrapper {\n /* 关键修改:更新 width 和 max-height */\n width: 80vw; \n max-height: 85vh; \n aspect-ratio: 16 / 9; \n position: relative; \n background-color: black; \n max-width: 100%; \n }\n /* 视频元素 */\n .video-player-wrapper #preview-video {\n position: absolute; \n top: 0;\n left: 0;\n width: 100%;\n height: 100%;\n display: block;\n }\n\n /* 画质控制盒 (底部按钮) */\n .video-control-box {\n display: flex;\n flex-direction: row; \n justify-content: center; \n flex-wrap: wrap; \n gap: 10px;\n padding: 10px 0; \n }\n\n /* 按钮样式 (保留) */\n .video-control-btn {\n min-width:80px;\n padding: 6px 12px;\n background: rgba(255,255,255,0.2);\n color: white;\n border: 1px solid rgba(255,255,255,0.5);\n border-radius: 4px;\n cursor: pointer;\n text-align: center;\n font-size: 14px;\n transition: background-color 0.2s, border-color 0.2s;\n }\n .video-control-btn:hover {\n background: rgba(255,255,255,0.4);\n }\n .video-control-btn.active {\n background-color: #1890ff; \n color: white;\n font-weight: bold;\n border: 1px solid #096dd9;\n }\n ";
}
@@ -22,7 +24,10 @@ class BusPreviewVideoPlugin extends BasePlugin {
}
closeVideoModal() {
const $previewVideo = $("#preview-video");
$previewVideo.length > 0 && $previewVideo[0].pause();
if ($previewVideo.length > 0) {
const videoEl = $previewVideo[0];
videoEl.pause();
}
$("#bus-preview-modal").removeClass("is-open");
}
async handle() {
@@ -51,7 +56,8 @@ class BusPreviewVideoPlugin extends BasePlugin {
let $previewVideo = $("#preview-video");
if ($previewVideo.length > 0) {
$modal.addClass("is-open");
$previewVideo[0].play().catch((e => console.warn("尝试播放失败 (可能被浏览器阻止):", e)));
const videoEl = $previewVideo[0];
videoEl.play().catch((e => console.warn("尝试播放失败 (可能被浏览器阻止):", e)));
return;
}
let carNum2 = this.getPageInfo().carNum;
@@ -70,13 +76,15 @@ class BusPreviewVideoPlugin extends BasePlugin {
defaultVideoQuality = selectDefaultQuality(Object.keys(dmmVideoMap), defaultVideoQuality);
let defaultVideoUrl = dmmVideoMap[defaultVideoQuality];
$container.html(`\n <div class="video-player-wrapper">\n <video id="preview-video" controls playsinline>\n <source src="${defaultVideoUrl}" />\n </video>\n </div>\n <div class="video-control-box">\n </div>\n `);
const $videoEl = $("#preview-video"), $previewSource = $videoEl.find("source"), $qualityControlsBox = $container.find(".video-control-box");
if (!$videoEl.length || !$previewSource.length) return;
const videoEl = $videoEl[0], jhs_videoMuted = localStorage.getItem("jhs_videoMuted");
const $videoEl = $("#preview-video"), $qualityControlsBox = $container.find(".video-control-box");
if (!$videoEl.length) return;
const videoEl = $videoEl[0];
const jhs_videoMuted = localStorage.getItem("jhs_videoMuted");
videoEl.muted = !jhs_videoMuted || "yes" === jhs_videoMuted;
videoEl.addEventListener("volumechange", (function() {
localStorage.setItem("jhs_videoMuted", videoEl.muted ? "yes" : "no");
}));
let buttonsHtml = "";
qualityOptions.forEach((option => {
let dmmVideoUrl = dmmVideoMap[option.quality];
@@ -92,10 +100,12 @@ class BusPreviewVideoPlugin extends BasePlugin {
const $button = $(e.currentTarget);
if ($button.hasClass("active")) return;
let videoSrc = $button.attr("data-video-src");
$previewSource.attr("src", videoSrc);
const currentTime = videoEl.currentTime;
videoEl.src = videoSrc;
videoEl.load();
videoEl.currentTime = currentTime;
if (currentTime > 0 && Number.isFinite(currentTime)) {
videoEl.currentTime = currentTime;
}
await videoEl.play();
$buttons.removeClass("active");
$button.addClass("active");
File diff suppressed because one or more lines are too long
+225 -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,45 @@ 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 defaultFullSortOrder = isJavBus ? ["date", "size"] : ["date", "size", "files"];
const fullSortOrder = (settings.sortOrder && settings.sortOrder.length === defaultFullSortOrder.length) ? settings.sortOrder : defaultFullSortOrder;
const effectiveSortOrder = fullSortOrder.filter(key => sortEnabled[key]);
const applySettings = {
filterHD: settings.filterHD || !1,
filter4K: settings.filter4K || !1,
filterSubtitle: settings.filterSubtitle || !1,
filterUncensored: settings.filterUncensored || !1,
sortOrder: effectiveSortOrder,
};
const rowReadyCondition = isJavBus
? (() => $("#magnet-table tr").length > 1)
: (() => $("#magnets-content .item[data-rank]").length > 0);
utils.loopDetector(rowReadyCondition, () => {
highlightMagnetPlugin.applySortAndFilter(applySettings);
});
$("#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 +92,195 @@ 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 || (isJavBus ? {date: !1, size: !1} : {date: !1, size: !1, files: !1});
const defaultSortOrder = isJavBus ? ["date", "size"] : ["date", "size", "files"];
const sortOrder = (settings.sortOrder && settings.sortOrder.length === defaultSortOrder.length) ? settings.sortOrder : defaultSortOrder;
const filterHD = settings.filterHD || !1;
const filter4K = settings.filter4K || !1;
const filterSubtitle = settings.filterSubtitle || !1;
const filterUncensored = settings.filterUncensored || !1;
const SORT_LABELS = isJavBus ? {date: "发布日期", size: "文件大小"} : {
date: "发布日期",
size: "文件大小",
files: "文件数量"
};
const SORT_HINTS = isJavBus ? {date: "从新到旧", size: "从大到小"} : {
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)}`);
+202 -23
View File
@@ -5,26 +5,211 @@ class HighlightMagnetPlugin extends BasePlugin {
getName() {
return "HighlightMagnetPlugin";
}
doFilterMagnet() {
this.handleDb();
this.handleBus();
parseDateFromAttr(dateStr) {
if (!dateStr) return 0;
if (dateStr.length === 8) {
const y = parseInt(dateStr.substring(0, 4), 10);
const m = parseInt(dateStr.substring(4, 6), 10) - 1;
const d = parseInt(dateStr.substring(6, 8), 10);
const date = new Date(y, m, d);
return isNaN(date.getTime()) ? 0 : date.getTime();
}
const d = new Date(dateStr);
return isNaN(d.getTime()) ? 0 : d.getTime();
}
handleDb() {
parseSizeToBytes(sizeText) {
if (!sizeText) return 0;
const s = sizeText.trim().toUpperCase();
const num = parseFloat(s);
if (isNaN(num)) return 0;
if (s.includes('TB')) return num * 1024 * 1024 * 1024 * 1024;
if (s.includes('GB')) return num * 1024 * 1024 * 1024;
if (s.includes('MB')) return num * 1024 * 1024;
if (s.includes('KB')) return num * 1024;
return num;
}
getRowInfo(row) {
const $row = $(row);
const nameText = $row.find('.name').text().toLowerCase();
const tags = [];
$row.find('.tags .tag').each((_, tag) => tags.push($(tag).text().toLowerCase()));
const sizeMB = parseInt($row.attr('data-size'), 10) || 0;
const files = parseInt($row.attr('data-files'), 10) || 0;
const dateStr = $row.attr('data-date') || '';
return {
row: row,
$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('無碼'),
};
}
getRowInfoForBus(row) {
const $row = $(row);
const $nameCell = $row.find('td:first-child');
const nameText = ($nameCell.find('a:first-child').text() || '').toLowerCase().trim();
const sizeText = ($row.find('td:nth-child(2)').text() || '').trim();
const dateText = ($row.find('td:nth-child(3)').text() || '').trim();
const hasHDButton = $nameCell.find('a.btn-primary').text().includes('高清');
return {
row: row,
$row: $row,
nameText: nameText,
date: this.parseDateFromAttr(dateText),
dateStr: dateText,
size: this.parseSizeToBytes(sizeText),
files: 0,
tags: [],
is4K: nameText.includes('4k') || nameText.includes('[4k]'),
isHD: hasHDButton || nameText.includes('高清'),
isSubtitle: nameText.includes('字幕') || nameText.includes('-c') || nameText.includes('.chs') || nameText.includes('.cht') || nameText.includes('.chi'),
isUncensored: nameText.includes('无码') || nameText.includes('無碼') || nameText.includes('-u') || nameText.includes('-uc'),
};
}
async applySortAndFilter(settings) {
if (isJavBus) {
return this.applySortAndFilterForBus(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());
}
applySortAndFilterForBus(settings) {
const $table = $("#magnet-table");
if (!$table.length) return;
const allRows = $table.find("tr").toArray();
const headerRow = allRows.find(r => $(r).find('td').length === 0 || $(r).css('font-weight') === 'bold' || $(r).css('fontWeight') === 'bold' || $(r).css('font-weight') === '700');
const dataRows = headerRow ? allRows.filter(r => r !== headerRow) : allRows.slice(1);
if (0 === dataRows.length) return;
const rowInfos = dataRows.map(row => this.getRowInfoForBus(row));
const {filterHD, filter4K, filterSubtitle, filterUncensored, sortOrder} = settings;
const anyFilterEnabled = filterHD || filter4K || filterSubtitle || filterUncensored;
let visibleInfos = rowInfos;
if (anyFilterEnabled) {
visibleInfos = rowInfos.filter(info => {
if (filterHD && info.isHD) return !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;
}
if (0 !== cmp) return cmp;
}
return 0;
});
}
const hiddenInfos = rowInfos.filter(info => !visibleInfos.includes(info));
const fragment = document.createDocumentFragment();
visibleInfos.forEach(info => fragment.appendChild(info.row));
hiddenInfos.forEach(info => fragment.appendChild(info.row));
const insertAnchor = headerRow || $table.find("tr").first()[0];
if (insertAnchor && insertAnchor.nextSibling) {
$(insertAnchor).after(fragment);
} else {
$table.append(fragment);
}
visibleInfos.forEach(info => {
if (info.is4K) {
info.$row.addClass('magnet-4k-highlight');
}
});
hiddenInfos.forEach(info => $(info.row).hide());
}
showAll() {
if (isJavDb) {
$("#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();
$(el).removeClass('magnet-4k-highlight');
}));
}
}
handleBus() {
isJavBus && window.isDetailPage && utils.loopDetector((() => $("#magnet-table td a").length > 0), (() => {
@@ -44,12 +229,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()));
}
}
+8 -1
View File
@@ -162,10 +162,17 @@ class ImageRecognitionPlugin extends BasePlugin {
type: mimeType
}), formData = new FormData;
formData.append("image", blob);
const idKey = "jhs_imgurClientId";
let clientId = localStorage.getItem(idKey);
if (!clientId) {
clientId = window.prompt("请输入您自己的Imgur Client-ID(可前往 https://api.imgur.com/oauth2/addclient 免费申请)用于图片上传搜索:");
if (!clientId) throw new Error("未提供Imgur Client-ID,无法上传图片");
localStorage.setItem(idKey, clientId);
}
const response = await fetch("https://api.imgur.com/3/image", {
method: "POST",
headers: {
Authorization: "Client-ID d70305e7c3ac5c6"
Authorization: `Client-ID ${clientId}`
},
body: formData
}), data = await response.json();
+239
View File
@@ -0,0 +1,239 @@
import {BasePlugin} from '../core/base-plugin.js';
import {__publicField, isJavBus, isJavDb, qualityOptions} from '../core/constants.js';
import {isM3U8Url, attachHlsToVideo, destroyHls, loadHlsLibrary, getHlsClass} from '../lib/hls-runtime.js';
import {getJavxyVideoUrls} from '../api/javxy.js';
class JavxyPreviewVideoPlugin extends BasePlugin {
getName() {
return "JavxyPreviewVideoPlugin";
}
_attachVideoSrc(videoEl, src) {
if (!videoEl || !src) return;
destroyHls(videoEl);
if (isM3U8Url(src)) {
const HlsClass = getHlsClass();
if (HlsClass) {
attachHlsToVideo(videoEl, src).catch((err) => {
clog.error('视频预览 HLS 加载失败:', err);
videoEl.src = src;
videoEl.load && videoEl.load();
});
} else {
loadHlsLibrary().then((HlsClass) => {
if (videoEl && videoEl.isConnected && HlsClass) {
attachHlsToVideo(videoEl, src).catch((err) => {
clog.error('视频预览 HLS 延迟加载失败:', err);
videoEl.src = src;
videoEl.load && videoEl.load();
});
} else if (videoEl && videoEl.isConnected) {
videoEl.src = src;
videoEl.load && videoEl.load();
}
});
}
} else {
videoEl.src = src;
videoEl.load && videoEl.load();
}
}
_selectDefaultQuality(qualityList, intendedDefault) {
if (!qualityList || qualityList.length === 0) return null;
const availableSet = new Set(qualityList);
if (availableSet.has(intendedDefault)) return intendedDefault;
const priorityOrder = qualityOptions.map(o => o.quality).reverse();
for (const q of priorityOrder) if (availableSet.has(q)) return q;
return qualityList[0];
}
async initCss() {
return `
.jhs-javxy-video-modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.95); z-index: 12345700;
display: flex; justify-content: center; align-items: center;
opacity: 0; visibility: hidden; transition: opacity 0.2s;
}
.jhs-javxy-video-modal.is-open { opacity: 1; visibility: visible; }
.jhs-javxy-video-modal-inner {
display: flex; flex-direction: column; align-items: center;
gap: 12px; max-width: 90vw; max-height: 90vh;
}
.jhs-javxy-video-wrapper {
width: 80vw; max-height: 80vh; aspect-ratio: 16/9;
background: #000; position: relative;
}
.jhs-javxy-video-wrapper video {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}
.jhs-javxy-quality-bar {
display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;
}
.jhs-javxy-quality-btn {
min-width: 60px; padding: 5px 10px; font-size: 13px;
background: rgba(255,255,255,0.2); color: #fff;
border: 1px solid rgba(255,255,255,0.5); border-radius: 4px;
cursor: pointer; transition: background 0.2s;
}
.jhs-javxy-quality-btn:hover { background: rgba(255,255,255,0.4); }
.jhs-javxy-quality-btn.active {
background: #1890ff; border-color: #096dd9; font-weight: bold;
}
.jhs-javxy-loading {
color: #fff; font-size: 16px; text-align: center;
}
`;
}
async handle() {
if (!window.isDetailPage) return;
const carNum = this.getPageInfo().carNum;
if (!carNum) return;
this._addJavxyButton(carNum);
}
_addJavxyButton(carNum) {
let $target = null;
if (isJavDb) {
$target = $('a[title="複製番號"]');
} else if (isJavBus) {
const headerSpans = document.querySelectorAll("span.header");
for (const span of headerSpans) {
if (span.textContent.trim() === "識別碼:") {
const nextSpan = span.nextElementSibling;
if (nextSpan && nextSpan.tagName === "SPAN") {
const copyBtn = nextSpan.nextElementSibling;
if (copyBtn && copyBtn.tagName === "BUTTON" && copyBtn.textContent.trim() === "复制") {
$target = $(copyBtn);
} else {
$target = $(nextSpan);
}
}
break;
}
}
}
if (!$target || !$target.length) return;
const $btn = $(`<a class="site-btn" style="min-width: auto; padding: 0 5px; margin-left: 6px; margin-bottom: 0; background-color: #1890ff; vertical-align: middle; cursor: pointer; font-size: 11px;"><span>视频预览</span></a>`);
$btn.on("click", async (e) => {
e.preventDefault();
e.stopPropagation();
$btn.css("pointer-events", "none").find("span").text("...");
try {
await this._openJavxyPlayer(carNum);
} finally {
$btn.css("pointer-events", "").find("span").text("视频预览");
}
});
$target.after($btn);
}
async _openJavxyPlayer(carNum) {
if ($("#jhs-javxy-video-modal").length === 0) {
$("body").append(`
<div id="jhs-javxy-video-modal" class="jhs-javxy-video-modal">
<div class="jhs-javxy-video-modal-inner">
<div class="jhs-javxy-video-wrapper">
<video id="jhs-javxy-video" controls playsinline></video>
</div>
<div class="jhs-javxy-quality-bar"></div>
</div>
</div>
`);
const $modal = $("#jhs-javxy-video-modal");
$modal.on("click", (e) => {
if (e.target.id === "jhs-javxy-video-modal") {
$modal.removeClass("is-open");
const videoEl = document.getElementById("jhs-javxy-video");
if (videoEl) {
destroyHls(videoEl);
videoEl.pause();
}
}
});
$(document).on("keydown", (e) => {
if (e.key === "Escape" && $modal.hasClass("is-open")) {
$modal.removeClass("is-open");
const videoEl = document.getElementById("jhs-javxy-video");
if (videoEl) {
destroyHls(videoEl);
videoEl.pause();
}
}
});
}
const $modal = $("#jhs-javxy-video-modal");
const $wrapper = $modal.find(".jhs-javxy-video-wrapper");
const $qualityBar = $modal.find(".jhs-javxy-quality-bar");
$wrapper.html('<div class="jhs-javxy-loading">视频预览加载中...</div>');
$qualityBar.empty();
$modal.addClass("is-open");
let videoMap = null;
try {
videoMap = await getJavxyVideoUrls(carNum);
} catch (err) {
$wrapper.html(`<div class="jhs-javxy-loading">视频预览请求失败: ${err.message}</div>`);
return;
}
if (!videoMap || Object.keys(videoMap).length === 0) {
$wrapper.html('<div class="jhs-javxy-loading">未找到可用的视频源</div>');
return;
}
const qualityList = Object.keys(videoMap);
const defaultQuality = this._selectDefaultQuality(qualityList, '720p');
const defaultUrl = videoMap[defaultQuality];
$wrapper.html(`<video id="jhs-javxy-video" controls playsinline>
<source src="${defaultUrl}" />
</video>`);
const videoEl = document.getElementById("jhs-javxy-video");
if (!videoEl) return;
if (isM3U8Url(defaultUrl)) {
this._attachVideoSrc(videoEl, defaultUrl);
}
let buttonsHtml = "";
qualityOptions.forEach((option) => {
const url = videoMap[option.quality];
if (url) {
buttonsHtml += `<button class="jhs-javxy-quality-btn${option.quality === defaultQuality ? ' active' : ''}"
data-quality="${option.quality}" data-video-src="${url}">${option.text}</button>`;
}
});
$qualityBar.html(buttonsHtml);
$qualityBar.off("click").on("click", ".jhs-javxy-quality-btn", (e) => {
const $btn = $(e.currentTarget);
if ($btn.hasClass("active")) return;
const src = $btn.attr("data-video-src");
const currentTime = videoEl.currentTime;
destroyHls(videoEl);
this._attachVideoSrc(videoEl, src);
videoEl.load();
if (currentTime > 0 && Number.isFinite(currentTime)) {
videoEl.currentTime = currentTime;
}
videoEl.play().catch(() => {
});
$qualityBar.find(".jhs-javxy-quality-btn").removeClass("active");
$btn.addClass("active");
});
videoEl.play().catch((e) => console.warn("视频预览播放失败:", e));
}
}
export {JavxyPreviewVideoPlugin};
+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");
+1051 -138
View File
File diff suppressed because it is too large Load Diff
+95 -96
View File
@@ -1,6 +1,6 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb } from '../core/constants.js';
import { AsyncQueue } from '../core/utils.js';
import {BasePlugin} from '../core/base-plugin.js';
import {__publicField, isJavBus, isJavDb} from '../core/constants.js';
import {AsyncQueue} from '../core/utils.js';
class OtherSitePlugin extends BasePlugin {
constructor() {
@@ -10,98 +10,97 @@ class OtherSitePlugin extends BasePlugin {
__publicField(this, "warnBackgroundColor", "#d7a80c");
__publicField(this, "domainErrorBackgroundColor", "#d7780c");
__publicField(this, "asyncQueue", new AsyncQueue);
__publicField(this, "siteConfigs", [ {
id: "javTrailersBtn",
getBaseUrl: async () => await this.getJavTrailersUrl(),
itemSelector: ".videos-list .video-link",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search/${carNum2}`,
getDetailPageHref: $box2 => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.find("p.card-text").text()
}, {
id: "123AvBtn",
getBaseUrl: async () => await this.getAv123Url() + "/ja",
itemSelector: ".box-item",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search?keyword=${carNum2}`,
getDetailPageHref: $box2 => $box2.find(".detail a").attr("href"),
findCarNumOrTitle: $box2 => $box2.find("img").attr("title")
}, {
id: "jableBtn",
getBaseUrl: async () => await this.getjableUrl(),
itemSelector: "#list_videos_videos_list_search_result .detail .title a",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search/${carNum2}/`,
getDetailPageHref: $box2 => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.text()
}, {
id: "avgleBtn",
getBaseUrl: async () => await this.getAvgleUrl(),
itemSelector: ".text-secondary",
searchPath: (baseUrl, carNum2) => `${baseUrl}/vod/search.html?wd=${carNum2}`,
getDetailPageHref: $box2 => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.text()
}, {
id: "missAvBtn",
getBaseUrl: async () => await this.getMissAvUrl(),
itemSelector: ".text-secondary",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search/${carNum2}`,
getDetailPageHref: $box2 => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.text()
}, {
id: "supJavBtn",
getBaseUrl: async () => await this.getSupJavUrl(),
itemSelector: ".posts post",
searchPath: (baseUrl, carNum2) => `${baseUrl}/?s=${carNum2}`,
getDetailPageHref: ($box2, baseUrl, carNum2) => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.attr("title")
}, {
id: "javDbBtn",
getBaseUrl: async () => await this.getJavDbUrl(),
itemSelector: ".movie-list .item",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search?q=${carNum2}`,
getDetailPageHref: $box2 => $box2.find("a").attr("href"),
findCarNumOrTitle: $box2 => $box2.find(".video-title").text(),
condition: sourceCarNum => isJavBus
}, {
id: "javBusBtn",
getBaseUrl: async () => await this.getJavBusUrl(),
itemSelector: ".container h3",
searchPath: (baseUrl, carNum2) => `${baseUrl}/${carNum2}`,
getDetailPageHref: ($box2, baseUrl, carNum2) => `${baseUrl}/${carNum2}`,
findCarNumOrTitle: $box2 => $box2.text(),
condition: sourceCarNum => isJavDb && sourceCarNum && !sourceCarNum.includes("FC2")
}, {
id: "fanzaBtn",
noHandle: !0,
initUrl: carNum2 => `https://www.dmm.co.jp/search/=/searchstr=${carNum2}`,
condition: sourceCarNum => sourceCarNum && !sourceCarNum.includes("FC2")
} ]);
__publicField(this, "siteConfigs", [
{
id: "MissAvBtn",
getBaseUrl: async () => await this.getMissAvUrl(),
itemSelector: ".text-secondary",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search/${carNum2}`,
getDetailPageHref: $box2 => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.text()
}, {
id: "123AvBtn",
getBaseUrl: async () => await this.getAv123Url() + "/ja",
itemSelector: ".box-item",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search?keyword=${carNum2}`,
getDetailPageHref: $box2 => $box2.find(".detail a").attr("href"),
findCarNumOrTitle: $box2 => $box2.find("img").attr("title")
}, {
id: "JableBtn",
getBaseUrl: async () => await this.getjableUrl(),
itemSelector: "#list_videos_videos_list_search_result .detail .title a",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search/${carNum2}/`,
getDetailPageHref: $box2 => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.text()
}, {
id: "SupJavBtn",
getBaseUrl: async () => await this.getSupJavUrl(),
itemSelector: ".posts post",
searchPath: (baseUrl, carNum2) => `${baseUrl}/?s=${carNum2}`,
getDetailPageHref: ($box2, baseUrl, carNum2) => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.attr("title")
},
{
id: "JavTrailersBtn",
getBaseUrl: async () => await this.getJavTrailersUrl(),
itemSelector: ".videos-list .video-link",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search/${carNum2}`,
getDetailPageHref: $box2 => $box2.attr("href"),
findCarNumOrTitle: $box2 => $box2.find("p.card-text").text()
},
{
id: "JavDbBtn",
getBaseUrl: async () => await this.getJavDbUrl(),
itemSelector: ".movie-list .item",
searchPath: (baseUrl, carNum2) => `${baseUrl}/search?q=${carNum2}`,
getDetailPageHref: $box2 => $box2.find("a").attr("href"),
findCarNumOrTitle: $box2 => $box2.find(".video-title").text(),
condition: sourceCarNum => isJavBus
}, {
id: "JavBusBtn",
getBaseUrl: async () => await this.getJavBusUrl(),
itemSelector: ".container h3",
searchPath: (baseUrl, carNum2) => `${baseUrl}/${carNum2}`,
getDetailPageHref: ($box2, baseUrl, carNum2) => `${baseUrl}/${carNum2}`,
findCarNumOrTitle: $box2 => $box2.text(),
condition: sourceCarNum => isJavDb && sourceCarNum && !sourceCarNum.includes("FC2")
}]);
__publicField(this, "settingCache", null);
__publicField(this, "lastFetchTime", 0);
__publicField(this, "CACHE_DURATION", 1e4);
}
getName() {
return "OtherSitePlugin";
}
async initCss() {
return "\n <style>\n .site-btn {\n position: relative !important;\n min-width: 80px;\n display: inline-block;\n padding: 5px 10px;\n color: white !important;\n background-color:#938585;\n text-decoration: none;\n border-radius: 4px;\n text-align: center;\n margin-bottom: 5px;\n }\n .site-btn:hover {\n color: white;\n transform: translateY(-2px);\n box-shadow: 0 2px 5px rgba(0,0,0,0.1);\n }\n .site-tag {\n position: absolute; \n top: -15px; \n right: 0; \n background-color: #ffc107; \n color: #333; \n font-size: 12px; \n padding: 2px 6px; \n border-radius: 4px;\n }\n </style>\n ";
}
async handle() {
window.isDetailPage && this.loadOtherSite().then();
}
async loadOtherSite(carNum2, sourceCarNum) {
if ("yes" !== await storageManager.getSetting("enableLoadOtherSite", "yes")) return;
carNum2 || (carNum2 = this.getPageInfo().carNum);
const enabledSites = this.getEnabledSites(), html = `\n <div id="otherSiteBox" class="panel-block" style="${isJavDb ? "margin-top:8px;font-size:13px" : "margin-top:10px;font-size:13px"}; user-select: none; ">\n <div style="display: flex;gap: 5px;flex-wrap: wrap">\n ${this.siteConfigs.map((config => {
config.sourceCarNum = sourceCarNum;
if (config.condition && !1 === config.condition(config.sourceCarNum)) return "";
return `<a target="_blank" class="site-btn" style="${enabledSites.includes(config.id) ? "" : "display:none"}" id="${config.id}"><span>${config.id.replace("Btn", "")}</span></a>`;
})).join("")}\n <a id="settingSiteBtn" class="site-btn"><span>设置</span></a>\n </div>\n </div>\n \n <div id="settingsArea" class="panel-block" style="display: none; margin-top:10px; margin-bottom: 10px; user-select: none; ">\n <div id="siteCheckboxes" style="display: flex;gap: 5px;flex-wrap: wrap">\n </div>\n </div>\n `;
const enabledSites = this.getEnabledSites(),
html = `\n <div id="otherSiteBox" class="panel-block" style="${isJavDb ? "margin-top:8px;font-size:13px" : "margin-top:10px;font-size:13px"}; user-select: none; ">\n <div style="display: flex;gap: 5px;flex-wrap: wrap">\n ${this.siteConfigs.map((config => {
config.sourceCarNum = sourceCarNum;
if (config.condition && !1 === config.condition(config.sourceCarNum)) return "";
return `<a target="_blank" class="site-btn" style="${enabledSites.includes(config.id) ? "" : "display:none"}" id="${config.id}"><span>${config.id.replace("Btn", "")}</span></a>`;
})).join("")}\n <a id="settingSiteBtn" class="site-btn"><span>跳转设置</span></a>\n </div>\n </div>\n \n <div id="settingsArea" class="panel-block" style="display: none; margin-top:10px; margin-bottom: 10px; user-select: none; ">\n <div id="siteCheckboxes" style="display: flex;gap: 5px;flex-wrap: wrap">\n </div>\n </div>\n `;
$(".movie-panel-info").append(html);
$(".container .info").append(html);
$("#javTrailersBtn").on("click", (async event => {
event.preventDefault();
let settingObj = await storageManager.getSetting();
const filterHotKey = settingObj.filterHotKey, favoriteHotKey = settingObj.favoriteHotKey, speedVideoHotKey = settingObj.speedVideoHotKey;
let href = $("#javTrailersBtn").attr("href"), url = href + `?handle=1&filterHotKey=${filterHotKey}&favoriteHotKey=${favoriteHotKey}&speedVideoHotKey=${speedVideoHotKey}`;
const filterHotKey = settingObj.filterHotKey, favoriteHotKey = settingObj.favoriteHotKey,
speedVideoHotKey = settingObj.speedVideoHotKey;
let href = $("#javTrailersBtn").attr("href"),
url = href + `?handle=1&filterHotKey=${filterHotKey}&favoriteHotKey=${favoriteHotKey}&speedVideoHotKey=${speedVideoHotKey}`;
event && (event.ctrlKey || event.metaKey) && (url = href);
utils.openPage(url, carNum2, !1, event);
}));
@@ -111,26 +110,15 @@ class OtherSitePlugin extends BasePlugin {
this.renderSettingsArea();
this.setupEventListeners();
}
async handleSite(carNum2, config) {
const $btn = $(`#${config.id}`);
if (config.initUrl) {
$btn.attr("href", config.initUrl(carNum2));
$btn.css("backgroundColor", this.warnBackgroundColor);
}
if (config.noHandle && !0 === config.noHandle) {
const dmmCacheKey = "jhs_other_site_dmm", dmmCachedResult = (localStorage.getItem(dmmCacheKey) ? JSON.parse(localStorage.getItem(dmmCacheKey)) : {})[carNum2];
if (dmmCachedResult) if ("single" === dmmCachedResult.type) {
$btn.attr("href", dmmCachedResult.url);
$btn.css("backgroundColor", this.okBackgroundColor);
} else if ("multiple" === dmmCachedResult.type) {
$btn.attr("href", dmmCachedResult.url);
$btn.append('<span class="site-tag" style="top:-15px">多结果</span>');
$btn.css("backgroundColor", this.okBackgroundColor);
}
} else try {
try {
if ($btn.attr("href")) return;
if (utils.isHidden($btn)) return;
const cacheKey = "jhs_other_site", cacheData = localStorage.getItem(cacheKey) ? JSON.parse(localStorage.getItem(cacheKey)) : {}, siteKey = carNum2 + "_" + config.id.replace("Btn", ""), cachedResult = cacheData[siteKey];
const cacheKey = "jhs_other_site",
cacheData = localStorage.getItem(cacheKey) ? JSON.parse(localStorage.getItem(cacheKey)) : {},
siteKey = carNum2 + "_" + config.id.replace("Btn", ""), cachedResult = cacheData[siteKey];
if (cachedResult) {
if ("single" === cachedResult.type) {
$btn.attr("href", cachedResult.url);
@@ -144,7 +132,8 @@ class OtherSitePlugin extends BasePlugin {
}
const baseUrl = await config.getBaseUrl(), searchUrl = config.searchPath(baseUrl, carNum2);
$btn.attr("href", searchUrl);
const html = await gmHttp.get(searchUrl, null, config.headers, !0), $dom = utils.htmlTo$dom(html), resultUrlList = [];
const html = await gmHttp.get(searchUrl, null, config.headers, !0), $dom = utils.htmlTo$dom(html),
resultUrlList = [];
$dom.find(config.itemSelector).each(((index, element) => {
const $box2 = $(element);
if (!config.findCarNumOrTitle($box2).toLowerCase().includes(carNum2.toLowerCase())) return;
@@ -202,6 +191,7 @@ class OtherSitePlugin extends BasePlugin {
}
}
}
async getSettingCache() {
const now = Date.now();
if (!this.settingCache || now - this.lastFetchTime > this.CACHE_DURATION) {
@@ -210,37 +200,44 @@ class OtherSitePlugin extends BasePlugin {
}
return this.settingCache;
}
async getMissAvUrl() {
return (await this.getSettingCache()).missAvUrl || "https://missav.live";
}
async getjableUrl() {
return (await this.getSettingCache()).jableUrl || "https://jable.tv";
}
async getAvgleUrl() {
return (await this.getSettingCache()).avgleUrl || "https://jav.rs";
}
async getJavTrailersUrl() {
return (await this.getSettingCache()).javTrailersUrl || "https://javtrailers.com";
}
async getAv123Url() {
return (await this.getSettingCache()).av123Url || "https://123av.com";
}
async getJavDbUrl() {
return (await this.getSettingCache()).javDbUrl || "https://javdb.com";
}
async getJavBusUrl() {
return (await this.getSettingCache()).javBusUrl || "https://www.javbus.com";
}
async getSupJavUrl() {
return (await this.getSettingCache()).supJavUrl || "https://supjav.com";
}
getEnabledSites() {
const enabledSites = localStorage.getItem("jhs_enabled_sites");
return enabledSites ? JSON.parse(enabledSites) : this.siteConfigs.map((c => c.id));
}
saveEnabledSites(sites) {
localStorage.setItem("jhs_enabled_sites", JSON.stringify(sites));
}
renderSettingsArea() {
const enabledSites = this.getEnabledSites(), checkboxesDiv = document.getElementById("siteCheckboxes");
checkboxesDiv && (checkboxesDiv.innerHTML = this.siteConfigs.map((config => {
@@ -248,6 +245,7 @@ class OtherSitePlugin extends BasePlugin {
return `\n <div style="margin-right: 15px; display: flex; align-items: ${isJavDb ? "center" : "flex-start"};">\n <input type="checkbox" id="checkbox-${config.id}" data-site-id="${config.id}" ${isEnabled ? "checked" : ""} style="margin-right: 8px; cursor: pointer;">\n <label for="checkbox-${config.id}" style="color: #333; font-weight: 500; cursor: pointer;">${config.id.replace("Btn", "")}</label>\n </div>\n `;
})).join(""));
}
setupEventListeners() {
const settingsArea = document.getElementById("settingsArea");
document.addEventListener("click", (event => {
@@ -261,7 +259,8 @@ class OtherSitePlugin extends BasePlugin {
const siteId = event.target.getAttribute("data-site-id");
if (event.target.checked) {
$(`#${siteId}`).show();
const carNum2 = this.getPageInfo().carNum, config = this.siteConfigs.find((item => item.id === siteId));
const carNum2 = this.getPageInfo().carNum,
config = this.siteConfigs.find((item => item.id === siteId));
this.handleSite(carNum2, config).then();
} else $(`#${siteId}`).hide();
const enabledSites = Array.from(settingsArea.querySelectorAll('input[type="checkbox"]:checked')).map((checkbox => checkbox.getAttribute("data-site-id")));
@@ -272,4 +271,4 @@ class OtherSitePlugin extends BasePlugin {
}
export { OtherSitePlugin };
export {OtherSitePlugin};
+41 -5
View File
@@ -1,5 +1,6 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, currentHref, NO, YES, qualityOptions } from '../core/constants.js';
import {getDmmVideo, selectDefaultQuality} from '../api/dmm.js';
class PreviewVideoPlugin extends BasePlugin {
getName() {
@@ -49,7 +50,7 @@ class PreviewVideoPlugin extends BasePlugin {
} else {
clog.debug("JavDB没有视频播放元素, 开始创建...");
const videoCoverSrc = $(".column-video-cover img").attr("src");
$(".preview-images").prepend(`\n <a class="preview-video-container" data-fancybox="gallery" href="#preview-video">\n <span>預告片</span>\n <img src="${videoCoverSrc}" class="video-cover" style="width: 150px; height: auto;" alt="">\n </a>\n `);
$(".preview-images").prepend(`\n <a class="preview-video-container" data-fancybox="gallery" href="#preview-video">\n <span>预告片</span>\n <img src="${videoCoverSrc}" class="video-cover" style="width: 150px; height: auto;" alt="">\n </a>\n `);
$(".preview-video-container").on("click", (event => {
utils.loopDetector((() => $(".fancybox-content #preview-video").length > 0), (async () => {
await this.handleVideo();
@@ -60,19 +61,52 @@ class PreviewVideoPlugin extends BasePlugin {
clog.error("预加载dmm失败:", error);
}
}
async handleVideo() {
if (await storageManager.getSetting("enableLoadPreviewVideo", YES) === NO) return;
const $videoEl = $("#preview-video");
if (!$videoEl.length) return;
const $videoContainer = $videoEl.parent();
$videoContainer.css("position", "relative");
const videoEl = $videoEl[0], jhs_videoMuted = localStorage.getItem("jhs_videoMuted");
jhs_videoMuted && (videoEl.muted = "yes" === jhs_videoMuted);
const videoEl = $videoEl[0];
let carNum2 = this.getPageInfo().carNum;
const jhs_videoMuted = localStorage.getItem("jhs_videoMuted");
if (jhs_videoMuted) videoEl.muted = "yes" === jhs_videoMuted;
videoEl.addEventListener("volumechange", (function() {
localStorage.setItem("jhs_videoMuted", videoEl.muted ? "yes" : "no");
}));
const CACHE_KEY = "jhs_dmm_video";
let dmmErrorRetryCount = 0;
const attachVideoErrorHandler = () => {
videoEl.addEventListener("error", async function onVideoError(e) {
if (dmmErrorRetryCount >= 2) return;
dmmErrorRetryCount++;
clog.warn(`预览视频加载失败(第${dmmErrorRetryCount}次),清除缓存后重试...`);
const cachedData = localStorage.getItem(CACHE_KEY) ? JSON.parse(localStorage.getItem(CACHE_KEY)) : {};
delete cachedData[carNum2];
localStorage.setItem(CACHE_KEY, JSON.stringify(cachedData));
const freshMap = await getDmmVideo(carNum2);
if (!freshMap) {
show.error("预览视频获取失败,请稍后重试");
return;
}
const storedQuality = await storageManager.getSetting("videoQuality");
const quality = selectDefaultQuality(Object.keys(freshMap), storedQuality);
const freshUrl = freshMap[quality];
const savedTime = videoEl.currentTime;
$videoEl.attr("src", freshUrl);
videoEl.load();
if (savedTime > 0 && Number.isFinite(savedTime)) {
videoEl.currentTime = savedTime;
}
videoEl.play();
}, {once: false});
};
attachVideoErrorHandler();
videoEl.play();
let carNum2 = this.getPageInfo().carNum;
const dmmVideoMap = await getDmmVideo(carNum2);
let $bottomToolbar = $("<div></div>").attr("id", "video-bottom-toolbar").css({
display: "flex",
@@ -122,7 +156,9 @@ class PreviewVideoPlugin extends BasePlugin {
const currentTime = videoEl.currentTime;
$videoEl.attr("src", videoSrc);
videoEl.load();
videoEl.currentTime = currentTime;
if (currentTime > 0 && Number.isFinite(currentTime)) {
videoEl.currentTime = currentTime;
}
await videoEl.play();
$bottomToolbar.find(".video-control-btn").removeClass("active").css({
"background-color": "#fff",
+20 -6
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -6,8 +6,9 @@ const getDownPathList = async () => {
return "object" == typeof res ? res.data : null;
};
const searchFiles = async (search_value, offset = 0, limit = 30) => {
const url = `https://webapi.115.com/files/search?search_value=${encodeURIComponent(search_value)}&offset=${offset}&limit=${limit}`;
const searchFiles = async (search_value, offset = 0, limit = 30, params = {}) => {
const queryParams = new URLSearchParams({search_value, offset, limit, ...params});
const url = `https://webapi.115.com/files/search?${queryParams}`;
return await gmHttp.get(url);
};
+207 -39
View File
@@ -275,7 +275,13 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
const $detail = $('\n <div class="jhs-match-detail jhs-115-match-detail" id="115-match-table">\n <table>\n <thead>\n <tr style="text-align: center">\n <th colspan="4">115匹配</th>\n </tr>\n <tr>\n <th>名称</th>\n <th>大小</th>\n <th>时间</th>\n <th>播放</th>\n </tr>\n </thead>\n <tbody>\n </tbody>\n </table>\n </div>\n '), $tbody = $detail.find("tbody");
try {
const carNum2 = this.getPageInfo().carNum;
matchList || (matchList = await this.searchFiles(carNum2));
if (!matchList) {
const {results, usedPhase2} = await this._searchFilesWithPhase(carNum2);
matchList = results;
if (usedPhase2 && matchList.length > 0) {
await this.autoRenameFiles(carNum2, matchList);
}
}
await this.checkLoginStatus();
if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $tbody.append(`<tr><td colspan="4">\n <a class='jhs-match-no-login-btn a-info'\n data-keyword="${carNum2}"\n title="未登录115网盘">未登录</a>\n </td></tr>`); else if (matchList.length > 0) {
const rowsHtml = matchList.map((match => `\n <tr>\n <td>${match.name}</td>\n <td>${this.formatSize(match.size)}</td>\n <td>${match.createTime}</td>\n <td>\n <a href="https://115vod.com/?pickcode=${match.videoId}&share_id=0"\n target="_blank"\n class="a-success"\n title="播放">播放</a>\n </td>\n </tr>\n `)).join("");
@@ -393,43 +399,56 @@ 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++;
onProgress && onProgress(i + 1, selectedItems.length, item, "未匹配到115文件");
continue;
}
const upperCarNum = item.carNum.toUpperCase();
const safeMatchList = matchList.filter(m => m.name.toUpperCase().includes(upperCarNum));
const {regex: safeRegex} = codeParse(item.carNum);
const safeMatchList = matchList.filter(m => safeRegex.test(m.name));
if (safeMatchList.length === 0) {
failedCount++;
onProgress && onProgress(i + 1, selectedItems.length, item, "无安全可删除目录");
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) {
const groupedByDir = {};
for (const m of safeMatchList) {
if (!groupedByDir[m.dirId]) groupedByDir[m.dirId] = [];
groupedByDir[m.dirId].push(m);
}
const entries = Object.entries(groupedByDir);
for (let g = 0; g < entries.length; g++) {
if (g > 0) {
await randomDelay();
}
const [dirId, items] = entries[g];
if (items.length === 1) {
const result = await this._deleteOne(dirId, false, i, selectedItems, item, onProgress);
if (result) {
itemDeleted++;
deletedCount++;
}
} catch (e) {
console.error(`删除目录失败: ${dirId}`, e);
} else {
for (let f = 0; f < items.length; f++) {
if (f > 0) await randomDelay();
const fileDeleted = await this._deleteOne(items[f].folderId, true, i, selectedItems, item, onProgress);
if (fileDeleted) {
itemDeleted++;
deletedCount++;
}
}
}
}
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,9 +458,63 @@ 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;
}
async _deleteOne(deleteId, isFile, batchIndex, selectedItems, item, onProgress) {
let retryCount = 0;
const maxRetries = 3;
const label = isFile ? "文件" : "目录";
while (retryCount <= maxRetries) {
try {
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
"fid[0]": deleteId,
ignore_warn: "1"
});
if (result && result.state) {
return 1;
} else {
const errMsg = (result && result.error) || "未知错误";
if (retryCount < maxRetries && this.isRateLimited(result)) {
retryCount++;
const backoff = (retryCount + 1) * 2000 + Math.random() * 1000;
onProgress && onProgress(batchIndex + 1, selectedItems.length, item, `风控触发,${backoff / 1000}秒后重试(${retryCount}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, backoff));
continue;
}
console.error(`删除${label}失败: ${deleteId}`, errMsg);
return 0;
}
} catch (e) {
if (retryCount < maxRetries && this.isRateLimited(e)) {
retryCount++;
const backoff = (retryCount + 1) * 2000 + Math.random() * 1000;
onProgress && onProgress(batchIndex + 1, selectedItems.length, item, `风控触发,${backoff / 1000}秒后重试(${retryCount}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, backoff));
continue;
}
console.error(`删除${label}失败: ${deleteId}`, e);
return 0;
}
}
return 0;
}
showDeleteDialog(carNum2, matchList, afterDelete) {
const upperCarNum = carNum2.toUpperCase();
const safeMatchList = matchList.filter(m => m.name.toUpperCase().includes(upperCarNum));
const {regex: safeRegex} = codeParse(carNum2);
const safeMatchList = matchList.filter(m => safeRegex.test(m.name));
const unsafeCount = matchList.length - safeMatchList.length;
if (safeMatchList.length === 0) {
show.error(`作品 "${carNum2}" 匹配到的 ${matchList.length} 个项目均不含番号,无法安全删除。请手动在115中删除。`);
@@ -494,13 +567,32 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
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);
const selectedItems = selected.map((i) => safeMatchList[i]);
const totalByDir = {};
for (const m of safeMatchList) {
totalByDir[m.dirId] = (totalByDir[m.dirId] || 0) + 1;
}
const groupedByDir = {};
for (const item of selectedItems) {
if (!groupedByDir[item.dirId]) groupedByDir[item.dirId] = [];
groupedByDir[item.dirId].push(item);
}
for (const [dirId, items] of Object.entries(groupedByDir)) {
if (items.length === totalByDir[dirId]) {
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);
} else {
for (const item of items) {
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
"fid[0]": item.folderId,
ignore_warn: "1"
});
result && result.state ? deletedCount++ : console.error(`删除文件失败: ${item.folderId}`, result);
}
}
}
loadObj.close();
show.ok(`作品 "${carNum2}" 已删除 ${deletedCount} 个文件夹`);
@@ -530,20 +622,76 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
window.open("https://115.com");
}
async searchFiles(carNum2) {
return (await this._searchFilesWithPhase(carNum2)).results;
}
async _searchFilesWithPhase(carNum2) {
const {regex} = codeParse(carNum2);
const searchKeyword = carNum2.toLowerCase().replace("fc2-", "");
const mapAndFilter = (rawData) => {
return rawData.map((data => ({
folderId: data.fid,
dirId: data.cid,
videoId: data.pc,
name: data.n,
createTime: utils.formatDate(new Date(1e3 * data.te)),
size: data.s,
isVideo: true
}))).filter((x => x.folderId && regex.test(x.name)));
};
var _a2;
let searchKeyword = carNum2.toLowerCase().replace("fc2-", "");
return (null == (_a2 = (await searchFiles(searchKeyword)).data) ? void 0 : _a2.map((data => ({
folderId: data.fid,
dirId: data.cid,
videoId: data.pc,
name: data.n,
createTime: utils.formatDate(new Date(1e3 * data.te)),
size: data.s,
isVideo: [ ".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv" ].some((ext => {
var _a3;
return null == (_a3 = data.n) ? void 0 : _a3.toLowerCase().endsWith(ext);
}))
}))).filter((x => x.folderId && x.isVideo && x.name.toLowerCase().includes(searchKeyword)))) || [];
const preciseData = (null == (_a2 = (await searchFiles(searchKeyword, 0, 30, {type: 4})).data) ? void 0 : _a2) || [];
const preciseResults = mapAndFilter(preciseData);
if (preciseResults.length > 0) return {results: preciseResults, usedPhase2: false};
const numberPart = extractNumberPart(carNum2);
if (numberPart === searchKeyword) return {results: preciseResults, usedPhase2: false};
const fuzzyData = (null == (_a2 = (await searchFiles(numberPart, 0, 30, {type: 4})).data) ? void 0 : _a2) || [];
return {results: mapAndFilter(fuzzyData), usedPhase2: true};
}
buildRenameTarget(carNum, originalName) {
const {regex} = codeParse(carNum);
const match = regex.exec(originalName);
if (!match) return null;
const before = originalName.slice(0, match.index);
const after = originalName.slice(match.index + match[0].length);
return before + carNum + after;
}
async autoRenameFiles(carNum, matchList) {
try {
const enableAutoRename = await storageManager.getSetting("enable115AutoRename", NO);
if (enableAutoRename !== YES) return;
const scope = await storageManager.getSetting("autoRenameScope", "vr_only");
if (scope === "vr_only" && !carNum.toUpperCase().includes("VR")) return;
const renameObj = {};
matchList.forEach(match => {
const newName = this.buildRenameTarget(carNum, match.name);
if (newName && newName !== match.name) {
renameObj[match.folderId] = newName;
}
});
if (Object.keys(renameObj).length === 0) return;
const formData = {};
Object.entries(renameObj).forEach(([fid, newName]) => {
formData[`files_new_name[${fid}]`] = newName;
});
const result = await gmHttp.postForm("https://webapi.115.com/files/batch_rename", formData);
if (result && result.state) {
Object.entries(renameObj).forEach(([fid, newName]) => {
const match = matchList.find(m => m.folderId === fid);
console.log(`[115重命名] ${match.name} → ${newName}`);
});
} else {
console.warn("[115重命名] 重命名失败:", (result && result.error) || "未知错误");
}
} catch (e) {
console.warn("[115重命名] 重命名失败,跳过:", e.message);
}
}
showMatchError($box2, carNum2, error) {
$box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn' data-keyword="${carNum2}" \n title="匹配失败,点击重试">匹配失败</a>`);
@@ -608,5 +756,25 @@ __publicField(_WangPan115MatchPlugin, "LoginStatus", {
let WangPan115MatchPlugin = _WangPan115MatchPlugin;
const extractNumberPart = (carNum) => {
let cleaned = carNum.replace(/^fc2[-_]?(ppv[-_])?/i, "");
const match = cleaned.match(/(\d+)$/);
return match ? match[1] : cleaned;
};
const codeParse = (code) => {
const codes = code.split(/-|_/);
const sep = "\\s?(0|-|_){0,2}\\s?";
let pattern = codes.join(sep);
if (/^fc2/i.test(code)) pattern = `${codes[0]}${sep}(ppv)?${sep}${codes.at(-1)}`;
return {
code,
codes,
prefix: codes[0],
regex: new RegExp(`(?<![a-z])${pattern}(?!\\d)`, "i"),
};
};
export { WangPan115Plugin, WangPan115MatchPlugin };
+5 -6
View File
@@ -6,10 +6,10 @@ export default defineConfig({
monkey({
entry: 'src/main.js',
userscript: {
name: 'JAV-JHS',
namespace: 'JAV-JHS',
version: '3.3.7',
author: 'xie bro,fireinrain',
name: 'JHS-enhance',
namespace: 'JHS-enhance',
version: '3.3.10',
author: 'xiebro,fireinrain',
description: 'Jav-鉴黄师 增强脚本。列表页:作品状态标签、一键筛选、新作品检测、演员黑名单过滤;详情页:磁力链接高亮、DMM 多画质预览视频、标题翻译、整合字幕搜索(迅雷+SubTitleCat)并支持 115 直传、多源预览图(javfree/projectjav/javstore)含来源切换;数据:115 网盘目录匹配与多目录选择、云盘备份/恢复、跨 Tab 同步;其他:自动翻页、分类折叠、Top250、以图识图、热门榜单、评论查看、相关清单。支持 JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers',
license: 'MIT',
icon: 'https://www.google.com/s2/favicons?sz=64&domain=javdb.com',
@@ -92,14 +92,13 @@ export default defineConfig({
build: {
fileName: 'jhs-enhance.user.js',
externalGlobals: {
'jquery': '$',
'jquery': 'jQuery',
'localforage': 'localforage',
'toastify-js': 'Toastify',
'blueimp-md5': 'md5',
'tabulator-tables': 'Tabulator',
'viewerjs': 'Viewer',
'qrcodejs': 'QRCode',
'layui-layer': 'layer',
},
},
}),