Compare commits

...

69 Commits

Author SHA1 Message Date
lzyme.dev@gmail.com 9d81f6a13a 美化资讯页面 2026-10-14 12:43:19 +08:00
lzyme.dev@gmail.com 0425ebf7c7 修复top250 2026-10-14 12:09:53 +08:00
lzyme.dev@gmail.com d636a2f683 修复鉴定记录 2026-10-12 16:32:49 +08:00
lzyme.dev@gmail.com 171ee3b9d7 修复排行榜 2026-10-12 01:30:17 +08:00
lzyme.dev@gmail.com 9d322cc76d 修复鉴定记录 2026-10-12 00:11:40 +08:00
lzyme.dev@gmail.com 89ab8bcff8 修复瀑布流加载出现错误 2026-10-11 23:49:51 +08:00
lzyme.dev@gmail.com fde1eadac9 修复高亮演员 2026-10-11 23:45:45 +08:00
lzyme.dev@gmail.com b81922103a 支持作品自定义保存目录 2026-10-11 23:12:30 +08:00
lzyme.dev@gmail.com e74f2cbdc8 修复磁力搜索获取失败刷新参数丢失 2026-09-10 16:32:00 +08:00
lzyme.dev@gmail.com 8396e3825d UI调整 2026-09-09 14:58:50 +08:00
lzyme.dev@gmail.com e4792cd55f 修复javbus离线下载按钮风格 2026-09-09 14:39:40 +08:00
lzyme.dev@gmail.com 72ec9a0e83 修复javbus下显示异常 2026-09-05 10:19:09 +08:00
lzyme.dev@gmail.com 4339f1b80f update 2026-09-03 23:45:08 +08:00
lzyme.dev@gmail.com 20d199f53a bump version 2026-09-03 21:46:01 +08:00
lzyme.dev@gmail.com a9e88cda88 移除javxy的接口依赖 2026-09-03 21:45:01 +08:00
lzyme.dev@gmail.com 032dcf1fdc 移除javxy的接口依赖 2026-09-03 19:23:21 +08:00
lzyme.dev@gmail.com 38221bc78d update 2026-09-02 23:26:37 +08:00
lzyme.dev@gmail.com 4230176ce4 修复github action发布版本号问题 2026-09-01 18:54:13 +08:00
lzyme.dev@gmail.com b5bdec2c35 更新文档 2026-09-01 15:28:19 +08:00
lzyme.dev@gmail.com 11c5650efb 新增下载高清封面 2026-09-01 15:24:40 +08:00
lzyme.dev@gmail.com 080cbb00ee 新增下载高清封面 2026-09-01 15:18:34 +08:00
lzyme.dev@gmail.com d5c59ba6ac 新增磁力列表验车按钮 2026-09-01 14:24:54 +08:00
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
lzyme.dev@gmail.com 2643a3d636 修复删除目录 2026-08-30 04:50:28 +08:00
lzyme.dev@gmail.com 34d34b5200 add missing lib 2026-08-30 04:17:27 +08:00
lzyme.dev@gmail.com cc8ff8082e 修复字幕选择作品文件夹无法展示问题 2026-08-30 04:15:23 +08:00
lzyme.dev@gmail.com 5636ee4f74 修复字幕选择作品文件夹无法展示问题 2026-08-30 04:05:40 +08:00
lzyme.dev@gmail.com b0fe131a03 修复视频长预览图 2026-08-30 03:49:06 +08:00
lzyme.dev@gmail.com 06e9f4e8b4 新增字幕直传115作品目录,支持多目录选择 2026-08-30 03:08:17 +08:00
lzyme.dev@gmail.com 8da3ef2ed5 修复瀑布流下滑删除按钮没有出现 2026-08-30 00:38:30 +08:00
lzyme.dev@gmail.com c645eb6629 瀑布流页面可以就行排序 2026-08-30 00:06:07 +08:00
lzyme.dev@gmail.com 0885b5e0fe 新增标记删除 2026-08-29 22:02:02 +08:00
lzyme.dev@gmail.com 268883ac02 强化目录删除的安全边界 保证只删除出现番号匹配的目录 2026-08-29 19:37:55 +08:00
lzyme.dev@gmail.com b1772160a1 详情页也添加删除按钮 2026-08-29 18:59:43 +08:00
lzyme.dev@gmail.com 39f252fa0b 多目录可选删除,暂时无法删除下载记录 2026-08-29 18:44:16 +08:00
lzyme.dev@gmail.com d99af94e74 新增作品列表下删除作品 2026-08-29 18:10:41 +08:00
lzyme.dev@gmail.com 2383406ede 新增作品列表下删除作品 2026-08-29 18:04:57 +08:00
lzyme.dev@gmail.com 8da5c36bf2 更新文档 2026-08-29 16:59:27 +08:00
47 changed files with 49567 additions and 10849 deletions
+6 -2
View File
@@ -27,11 +27,15 @@ jobs:
- name: Build
run: npm run build
- name: Get version
id: version
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload to Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ github.run_number }}
name: Release v${{ github.run_number }}
tag_name: v${{ steps.version.outputs.VERSION }}
name: Release v${{ steps.version.outputs.VERSION }}
files: dist/jhs-enhance.user.js
draft: false
prerelease: false
+1 -4
View File
@@ -23,7 +23,6 @@
// @exclude https://*javsee*/*actresses
// @exclude https://*seejav*/forum/*
// @exclude https://*seejav*/*actresses
// @require https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js
// @require https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js
// @require https://cdn.jsdelivr.net/npm/tabulator-tables@6.3.1/dist/js/tabulator.min.js
// @require https://cdn.jsdelivr.net/npm/layui-layer@1.0.9/dist/layer.min.js
@@ -10957,6 +10956,4 @@ pluginManager.processCss().then();
duration: -1
});
pluginManager.processPlugins().then();
}();
}();
+155 -84
View File
@@ -2,7 +2,7 @@
> Jav-鉴黄师 油猴脚本 — 工程化重构版本
基于 `vite-plugin-monkey` 脚手架,将原 10,000+ 行单体脚本拆分为 49 个可维护的 ES Module 模块,支持本地开发热更新、一键构建。
基于 `vite-plugin-monkey` 脚手架,将原 10,000+ 行单体脚本拆分为 56 个可维护的 ES Module 模块,支持本地开发热更新、一键构建。
---
@@ -12,53 +12,80 @@
### 一键安装
[![Install](https://img.shields.io/badge/点击安装-JHS--enhance.user.js-brightgreen)](https://github.com/替换为你的用户名/JHS-enhance/releases/latest/download/jhs-enhance.user.js)
[![Install](https://img.shields.io/badge/点击安装-JHS--enhance.user.js-brightgreen)](https://github.com/fireinrain/JHS-enhance/releases/latest/download/jhs-enhance.user.js)
> 点击上方按钮,Tampermonkey 会自动弹出安装提示。安装后脚本会自动更新(Tampermonkey 默认每天检查一次更新)。
### 手动安装
1. 从 [Releases](https://github.com/替换为你的用户名/JHS-enhance/releases) 页面下载最新 `jhs-enhance.user.js`
1. 从 [Releases](https://github.com/fireinrain/JHS-enhance/releases) 页面下载最新 `jhs-enhance.user.js`
2. 打开 Tampermonkey 管理面板 → 拖入文件安装
---
## 自动部署配置
本项目使用 GitHub Actions 在推送代码时自动构建并发布到 [GitHub Releases](https://github.com/替换为你的用户名/JHS-enhance/releases)。首次使用只需一步:
### 修改 README 中的安装链接
将 README.md 中所有 `替换为你的用户名` 替换为你的 GitHub 用户名:
```
https://github.com/你的用户名/JHS-enhance/releases/latest/download/jhs-enhance.user.js
```
推送代码后,GitHub Actions 自动构建并将脚本上传到 Release,无需额外配置。
### 工作原理
```
git push → GitHub Actions 自动构建 → 发布 GitHub Release
↓
https://github.com/用户名/JHS-enhance/releases/latest/download/jhs-enhance.user.js
↓
用户点击安装链接 → Tampermonkey 弹出安装提示
```
> **为什么不用 GitHub Pages?** Release 方案更简单,不需要配置 Pages,不需要额外权限,`/releases/latest/download/` 是固定链接,每次发版自动指向最新脚本。
---
## 功能概览
- **列表页增强**:作品状态标签(屏蔽/收藏/已下载/已观看)、一键打开待鉴定/已收藏作品、新作品检测、演员黑名单过滤
- **详情页增强**:磁力链接高亮、预览视频(DMM 多画质)、标题翻译、字幕搜索(SubTitleCat / 迅雷)、演员信息、相关清单
- **数据管理**:IndexedDB 本地存储、云盘备份/恢复(阿里云盘 / 115 网盘 / WebDAV)、跨 Tab 数据同步
- **自动化任务**:自动翻页、新作品检测、收藏演员同步、黑名单更新
- **UI 优化**:分类折叠、热榜 Top250、免 VIP 查看热播、以图识图、截图功能
- **多站点支持**: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 多画质预览视频
- **多源视频预览**:DMMRSC 聚合 API 独立按钮(番号/识别码右侧),HLS 支持、播放进度记忆、画质切换
- **标题翻译**:Google 翻译,支持开关多次切换不失效
- **字幕搜索**:合并迅雷 + SubTitleCat 为单一入口,支持 115 网盘直传、详情页预匹配目录、多目录选择上传、长目录名截断展示、字幕预览绿色字体
- **多源预览图**:javfree / projectjav / javstore 三源切换,来源按钮绿色高亮,sessionStorage 缓存,错误提示弹窗内显示
- 演员信息展示、相关清单、评论查看
- **下载高清封面**:获取 DMM 高清封面直链并下载,支持番号格式校验(排除 FC2/无码作品)
- 以图识图、第三方站点跳转
### 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
---
@@ -70,61 +97,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/ # 插件层(38 个功能插件)
│ ├── list-page.js # 列表页过滤与状态标签
│ ├── list-page-button.js # 列表页功能按钮(待鉴定/已收藏/新作品/黑名单)
│ ├── detail-page.js # 详情页数据加载
│ ├── detail-page-button.js# 详情页操作按钮
│ ├── 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(备选视频源回退)
│ │ └── dmmrsc.js # DMMRSC 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 # 截图功能
│ ├── magnet-hub.js # MagnetHub 磁力搜索
│ ├── other-site.js # 第三方站点链接
│ ├── jav-trailers.js # JavTrailers 站点
│ ├── subtitle-cat.js # SubTitleCat 字幕搜索
│ ├── 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# DMMRSC 多源视频预览(独立按钮、聚合 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 # 原始单体脚本(参考归档)
```
---
@@ -254,6 +289,12 @@ window.show.ok('操作成功'); // Toast 通知
---
## 赞助
如果当前项目有帮到您的话,赞助我喝瓶水/Star项目也是极好的,感谢支持 :)
![赞助](https://i.imgur.com/ynGK6he.jpeg)
## 构建配置
`vite-plugin-monkey` 在 `vite.config.mjs` 中配置了:
@@ -267,7 +308,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**
- 新增:DMMRSC 多源视频预览独立按钮(番号/识别码右侧),基于 DMMRSC 聚合 API
- 新增:HLS 视频播放运行时(hls.js 封装),支持 .m3u8 格式、播放进度记忆、画质切换
- 新增:DMM GraphQL API 模块,作为备选视频源回退方案
- 优化:`bus-preview-video.js` 和 `preview-video.js` 清理移植代码,保持原有播放逻辑不变
- 优化:`dmm.js` 回退至仅 DMM Affiliate API + GraphQL API 双通道
- **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 开发热更新
---
-251
View File
@@ -1,251 +0,0 @@
好的,让我先全面分析这个 11000 行的脚本。我会分段读取,理解它的整体结构和功能模块。
The user wants me to analyze the JHS-origin.js file (about 11,000 lines) and provide a detailed report on how to modularize it. Let me first read through the entire file to understand its structure, then provide a comprehensive refactoring plan.
Since the file is 10,962 lines, I need to read it in chunks. Let me start by reading several sections to understand the code structure.
Let me read the file in multiple chunks to understand:
1. The beginning (already read some)
2. Key sections throughout
3. The end
I'll read multiple sections in parallel.
Let me continue reading more sections to get a complete picture of the codebase.
Let me also read the end of the file to see the initialization/entry point.
Now I have a very comprehensive understanding of the codebase. Let me also read the very end to see the initialization.
Now I have a comprehensive understanding of the entire codebase. Let me write a detailed analysis report.
The code is approximately 10,962 lines, with a well-defined plugin architecture:
- PluginManager: manages plugins
- BasePlugin: base class for all plugins
- ~40 plugins extending BasePlugin
The code also contains:
- Utility classes (Utils, gmHttp, StorageManager, show, clog, etc.)
- CSS styles
- Constants and configuration
- API interfaces (JavDbApi, etc.)
- A plugin system with registration and execution
Let me now write a comprehensive refactoring report.
现在我已经完整分析了整个代码库。以下是详细的结构分析报告:
---
## 🔍 JHS-enhance 代码结构分析报告
### 📊 总体概况
| 指标 | 数据 |
|------|------|
| 总行数 | **10,962 行** |
| 核心类/插件 | **40+ 个** |
| 支持站点 | JavDB、JavBus、JavTrailers、SubTitleCat、阿里云盘、115网盘 |
| 外部依赖 | jQuery、Tabulator、layui-layer、MD5、Toastify、localForage、ViewerJS、QRCodeJS |
---
### 🏗️ 现有架构
代码已经有一个**不错的插件架构**,只是全部写在一个文件里:
```
入口(最后100行)
├── 初始化 pluginManager
├── 检测当前页面类型(isDetailPage / isListPage / isFc2Page)
├── 按站点注册插件
├── pluginManager.processCss() → 加载所有插件CSS
└── pluginManager.processPlugins() → 执行所有插件
```
插件基类 `BasePlugin` 提供了:
- `getName()` — 插件名
- `initCss()` — 返回CSS字符串
- `handle()` — 主逻辑入口
- `getPageInfo()` — 获取当前页面信息
- `getBean(name)` — 获取其他插件实例
---
### 📦 代码功能模块划分
我将 11000 行代码按功能分为以下 **8 大模块**,建议拆分为独立文件:
---
#### 模块一:🛠️ 基础设施层(~1500 行)
**建议目录:`src/core/`**
| 文件 | 内容 | 说明 |
|------|------|------|
| `utils.js` | `Utils` 类(~300行) | 通用工具函数:下载、滚动、剪贴板、URL解析、排序等 |
| `gmHttp.js` | `gmHttp` 类(~200行) | 基于 `GM_xmlhttpRequest` 的 HTTP 封装 |
| `storage.js` | `StorageManager` 类(~500行) | IndexedDB 存储管理:收藏、屏蔽、设置、演员等 |
| `show.js` | `show` / `clog` 对象(~100行) | Toast 消息提示、控制台日志 |
| `constants.js` | 所有常量(~200行) | 状态枚举、画质选项、翻译映射表、标签样式等 |
| `styles.js` | CSS 样式(~300行) | JavDB/JavBus 站点样式、全局样式、滚动条样式 |
---
#### 模块二:🔌 插件系统(~200 行)
**建议目录:`src/core/`**
| 文件 | 内容 | 说明 |
|------|------|------|
| `plugin-manager.js` | `PluginManager` 类 | 插件注册、CSS加载、插件执行 |
| `base-plugin.js` | `BasePlugin` 类 | 插件基类,提供 `getName()`、`handle()`、`initCss()` 等 |
---
#### 模块三:🌐 API 接口层(~200 行)
**建议目录:`src/api/`**
| 文件 | 内容 | 说明 |
|------|------|------|
| `javdb-api.js` | `javDbApi` 对象 | JavDB API:获取评论、搜索电影、电影详情、磁力链接 |
---
#### 模块四:📋 列表页插件(~2500 行)
**建议目录:`src/plugins/list/`**
| 文件 | 类名 | 功能 |
|------|------|------|
| `list-page.js` | `ListPagePlugin` | 列表页核心:状态标签渲染、屏蔽过滤、演员屏蔽、排序 |
| `list-page-button.js` | `ListPageButtonPlugin` | 列表页工具栏按钮:一键屏蔽、收藏、随机打开等 |
| `auto-page.js` | `AutoPagePlugin` | 自动翻页 |
| `fold-category.js` | `FoldCategoryPlugin` | 折叠/展开分类标签区域 |
| `cover-button.js` | `CoverButtonPlugin` | 封面图上的操作按钮(屏蔽、收藏等) |
| `history.js` | `HistoryPlugin` | 鉴定记录面板 |
| `filter-title-keyword.js` | `FilterTitleKeywordPlugin` | 标题关键词屏蔽 |
| `blacklist.js` | `BlacklistPlugin` | 黑名单管理(屏蔽演员/标签) |
| `hit-show.js` | `HitShowPlugin` | 热播榜 |
| `top250.js` | `TOP250Plugin` | TOP250 排行榜 |
---
#### 模块五:🎬 详情页插件(~2000 行)
**建议目录:`src/plugins/detail/`**
| 文件 | 类名 | 功能 |
|------|------|------|
| `detail-page.js` | `DetailPagePlugin` | 详情页核心:FC2/普通影片信息展示 |
| `detail-page-button.js` | `DetailPageButtonPlugin` | 详情页按钮:屏蔽、收藏、已下载、已观看、磁力搜索、字幕 |
| `bus-detail-page.js` | `BusDetailPagePlugin` | JavBus 详情页 |
| `review.js` | `ReviewPlugin` | 评论查看 |
| `related.js` | `RelatedPlugin` | 相关清单 |
| `highlight-magnet.js` | `HighlightMagnetPlugin` | 磁力链接高亮过滤 |
| `translate.js` | `TranslatePlugin` | 标题翻译(日→中) |
| `want-watched.js` | `WantAndWatchedVideosPlugin` | 想看/已看视频 |
---
#### 模块六:🎥 视频与媒体插件(~1500 行)
**建议目录:`src/plugins/media/`**
| 文件 | 类名 | 功能 |
|------|------|------|
| `preview-video.js` | `PreviewVideoPlugin` | JavDB 预览视频播放 |
| `bus-preview-video.js` | `BusPreviewVideoPlugin` | JavBus 预览视频播放 |
| `screen-shot.js` | `ScreenShotPlugin` | 缩略图加载 |
| `bus-img.js` | `BusImgPlugin` | JavBus 图片处理 |
| `image-recognition.js` | `ImageRecognitionPlugin` | 以图识图 |
---
#### 模块七:🔗 外部站点与工具插件(~2000 行)
**建议目录:`src/plugins/external/`**
| 文件 | 类名 | 功能 |
|------|------|------|
| `magnet-hub.js` | `MagnetHubPlugin` | 多站点磁力搜索聚合 |
| `other-site.js` | `OtherSitePlugin` | 其他站点入口(JavStore、MissAV等) |
| `fc2.js` | `Fc2Plugin` | FC2 影片页面 |
| `fc2-123av.js` | `Fc2By123AvPlugin` | 123AV 的 FC2 页面 |
| `wangpan-115.js` | `WangPan115Plugin` / `WangPan115TaskPlugin` / `WangPan115MatchPlugin` | 115网盘离线下载 |
| `aliyun-pan.js` | `AliyunPanPlugin` | 阿里云盘 |
| `jav-trailers.js` | `JavTrailersPlugin` | JavTrailers 站点 |
| `subtitle-cat.js` | `SubTitleCatPlugin` | SubTitleCat 字幕搜索 |
---
#### 模块八:👤 演员与任务插件(~2000 行)
**建议目录:`src/plugins/actress/`**
| 文件 | 类名 | 功能 |
|------|------|------|
| `actress-info.js` | `ActressInfoPlugin` | 演员信息面板 |
| `favorite-actresses.js` | `FavoriteActressesPlugin` | 收藏演员管理 |
| `new-video.js` | `NewVideoPlugin` | 新作品检测 |
| `task.js` | `TaskPlugin` | 定时任务:检测黑名单、同步演员、新作品检测 |
| `nav-bar.js` | `NavBarPlugin` / `BusNavBarPlugin` | 导航栏增强 |
| `setting.js` | `SettingPlugin` | 设置面板 |
---
### 🎯 建议的项目目录结构
```
JHS-enhance/
├── src/
│ ├── main.js # 入口:初始化 + 插件注册
│ ├── core/
│ │ ├── utils.js # 通用工具函数
│ │ ├── gmHttp.js # GM_xmlhttpRequest 封装
│ │ ├── storage.js # IndexedDB 存储管理
│ │ ├── show.js # Toast 消息提示
│ │ ├── constants.js # 所有常量
│ │ ├── styles.js # CSS 样式
│ │ ├── plugin-manager.js # 插件管理器
│ │ └── base-plugin.js # 插件基类
│ ├── api/
│ │ └── javdb-api.js # JavDB API 接口
│ ├── plugins/
│ │ ├── list/ # 列表页插件 (10个)
│ │ ├── detail/ # 详情页插件 (8个)
│ │ ├── media/ # 视频/媒体插件 (5个)
│ │ ├── external/ # 外部站点插件 (8个)
│ │ └── actress/ # 演员/任务插件 (6个)
│ └── styles/
│ └── ... (可选,如果CSS拆出来)
├── vite.config.mjs
├── package.json
└── JHS-origin.js # 保留原始文件作为参考
```
---
### ⚠️ 拆分时的注意事项
1. **全局变量依赖**:当前代码大量使用 `unsafeWindow.xxx = window.xxx`,拆分为 ES Module 后需要改为 `export`/`import`,同时通过 `window` 暴露给油猴全局作用域
2. **插件间依赖**:插件通过 `this.getBean(name)` 互相调用,拆分后需确保 `PluginManager` 先初始化
3. **CDN 外部依赖**:jQuery、Tabulator 等通过 `@require` 加载,在模块中直接使用 `$`、`Tabulator` 等全局变量即可
4. **CSS 注入**:`insertStyle()` 和 `initCss()` 需要 `document.head` 可用后才能执行
5. **循环依赖**:注意 `TaskPlugin` ↔ `BlacklistPlugin`、`ListPagePlugin` ↔ `BlacklistPlugin` 等之间的双向引用
---
### 🚀 建议的实施顺序
| 优先级 | 步骤 | 预计工作量 |
|--------|------|-----------|
| P0 | 拆分 `core/` 基础设施(utils, gmHttp, storage, show, constants, styles) | 1-2小时 |
| P1 | 拆分插件系统(plugin-manager, base-plugin)和入口 `main.js` | 30分钟 |
| P2 | 拆分 API 层(javdb-api) | 15分钟 |
| P3 | 分批拆分插件,每批 3-5 个,逐个验证构建 | 3-5小时 |
| P4 | 清理全局变量,统一为 ES Module 导入导出 | 1小时 |
---
需要我开始执行拆分吗?建议从 **P0(基础设施层)** 开始,这是最独立、最不容易出错的模块。
+40817 -9985
View File
File diff suppressed because one or more lines are too long
+993
View File
@@ -0,0 +1,993 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="renderer" content="webkit">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - JavBus</title>
<meta name="keywords" content="NIMA-086,肉漫,潮吹,中出,高畫質,原作コラボ,巨乳,アクメ・オーガズム,單體作品,DMM獨家">
<meta name="description"
content="【發行日期】2026-09-25,【長度】120分鐘,(NIMA-086)実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒">
<link rel="alternate" href="https://www.javbus.com/en/NIMA-086" hreflang="en"/>
<link rel="alternate" href="https://www.javbus.com/ja/NIMA-086" hreflang="ja"/>
<link rel="alternate" href="https://www.javbus.com/ko/NIMA-086" hreflang="ko"/>
<link rel="alternate" href="https://www.javbus.com/NIMA-086" hreflang="zh"/>
<link rel="alternate" href="https://www.javbus.com/NIMA-086" hreflang="x-default"/>
<link rel="canonical" href="https://www.javbus.com/NIMA-086"/>
<link rel="preconnect" href="https://poweredby.jads.co" crossorigin>
<link rel="dns-prefetch" href="https://poweredby.jads.co">
<link rel="stylesheet" href="https://www.javbus.com/css/bootstrap.min.css">
<link rel="stylesheet" href="https://www.javbus.com/css/bootstrap-theme.min.css">
<link rel="stylesheet" href="https://www.javbus.com/css/css-slider.css?v=9.3">
<link rel="stylesheet" href="https://www.javbus.com/css/nav.overlay.css?v=3.9.8">
<link rel="stylesheet" href="https://www.javbus.com/css/magnific-popup.css">
<link rel="stylesheet" href="https://www.javbus.com/css/base.css?v=7.15">
<script src="https://www.javbus.com/js/jquery.min.js"></script>
<script src="https://www.javbus.com/js/jquery.magnific-popup.min.js"></script>
<script src="https://www.javbus.com/js/bootstrap.min.js" defer></script>
<script src="https://www.javbus.com/js/jquery.cookie.min.js" defer></script>
<script src="https://www.javbus.com/js/base.js" defer></script>
<script src="https://www.javbus.com/js/bootstrap-hover-dropdown.js" defer></script>
<script src="https://www.javbus.com/js/nav.overlay.js?v=10.30.3" defer></script>
<!--[if lt IE 9]>
<script src="https://www.javbus.com/js/html5shiv.min.js"></script>
<script src="https://www.javbus.com/js/respond.min.js"></script><![endif]-->
</head>
<body>
<script language="JavaScript">
var mod = 0;
var lang = 'zh';
var info = '搜尋 識別碼, 影片, 演員';
function searchs(obj) {
var searchinput = $("#" + obj);
if (searchinput.val() == '') {
$('#magnet-url-post').trigger("click");
return false;
} else {
//$('#search-loading').show();str.replace(/ /g, "-")
window.open("https://www.javbus.com/search/" + encodeURIComponent($.trim(searchinput.val()).replace(/ /g, "+")) + "&type=&parent=ce");
}
}
$(function () {
var url = 'https://www.javbus.com/ajax/search-modal.php?floor=' + Math.floor(Math.random() * 1000 + 1) + '&lang=' + lang;
$.ajax({
url: url, type: 'GET', success: function (msg) {
$("#searchModal").append(msg);
}
});
});
</script>
<div id="search-loading">
<table class="search-loading-box" border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td align="center">
<table height="80" width="100%" border="0" cellpadding="0" cellspacing="0">
<tbody>
<tr>
<td height="40" align="center">
<div class="search-loading-text">搜尋中...</div>
</td>
</tr>
<tr>
<td height="40" align="center">
<img src="https://www.javbus.com/images/search_loading.gif" border="0">
</td>
</tr>
</tbody>
</table>
</td>
</tr>
</tbody>
</table>
</div>
<!-- Modal Search -->
<div id="searchModal" class="modal fade" tabindex="-1" role="dialog"></div>
<nav class="navbar navbar-default navbar-fixed-top top-bar" style="z-index:900">
<div class="container-fluid">
<div class="navbar-header mh50">
<a href="https://www.javbus.com/">
<img class="hidden-xs" height="50" alt="JavBus" src="https://www.javbus.com/images/logo.png"
style="height:40px; margin-top:5px;">
<img class="visible-xs-inline" height="50" alt="JavBus" src="https://www.javbus.com/images/logo.png">
</a>
<div class="btn-group pull-right visible-xs-inline" role="group" style="margin:8px 8px 0 0;"
onclick="window.location.href= '/forum/member.php?mod=logging&action=login&referer=%2F%2Fwww.javbus.com%2FNIMA-086'">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
aria-expanded="false">
<img class="avatar" style="width:22px; height:22px;"
src="https://uc.javbus22.com/uc/avatar.php?uid=0&size=small"></button>
</div>
<div class="btn-group pull-right visible-xs-inline" role="group" style="margin:8px 8px 0 0;">
<button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown"
aria-expanded="false">
<span class="glyphicon glyphicon-globe"></span> <span class="caret"></span>
</button>
<ul class="dropdown-menu" role="menu">
<li><a href="https://www.javbus.com/en/NIMA-086">English</a></li>
<li><a href="https://www.javbus.com/ja/NIMA-086">日本语</a></li>
<li><a href="https://www.javbus.com/ko/NIMA-086">한국의</a></li>
<li><a href="https://www.javbus.com/NIMA-086">中文</a></li>
</ul>
</div>
</div>
<div id="navbar" class="collapse navbar-collapse">
<div class="navbar-form navbar-left fullsearch-form">
<div class="input-group">
<input id="search-input" type="text" class="form-control" placeholder="搜尋 識別碼, 影片, 演員">
<span class="input-group-btn">
<button class="btn btn-default" type="submit" onClick="searchs('search-input')">搜尋</button>
</span>
</div>
</div>
<ul class="nav navbar-nav">
<li class="active"><a href="https://www.javbus.com/">有碼</a></li>
<li><a href="https://www.javbus.com/uncensored">無碼</a></li>
<li class="hidden-md hidden-sm"><!-- <div class="icon-new"></div> --><a href="https://www.javbus.hair/">歐美</a>
</li>
<li class="hidden-sm"><a href="https://www.javbus.com/forum/">論壇</a></li>
<li class="dropdown hidden-sm ">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-hover="dropdown" role="button"
aria-expanded="false">類別 <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="https://www.javbus.com/genre">有碼類別</a></li>
<li><a href="https://www.javbus.com/uncensored/genre">無碼類別</a></li>
</ul>
</li>
<li class="dropdown hidden-sm ">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-hover="dropdown" role="button"
aria-expanded="false">女優 <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="https://www.javbus.com/actresses">有碼女優</a></li>
<li><a href="https://www.javbus.com/uncensored/actresses">無碼女優</a></li>
</ul>
</li>
<li class="dropdown"><a href="https://www.javbus.com/" class="dropdown-toggle" data-toggle="dropdown"
data-hover="dropdown" role="button" aria-expanded="false"><span
class="glyphicon glyphicon-menu-hamburger"></span></a>
<ul class="dropdown-menu" role="menu">
<li class="visible-md-block visible-sm-block"><a href="https://www.javbus.hair/">歐美</a></li>
<li class="visible-sm-block"><a href="https://www.javbus.com/forum/">論壇</a></li>
<li class="visible-sm-block"><a href="https://www.javbus.com/genre">有碼類別</a></li>
<li class="visible-sm-block"><a href="https://www.javbus.com/uncensored/genre">無碼類別</a></li>
<li class="visible-sm-block"><a href="https://www.javbus.com/actresses">有碼女優</a></li>
<li class="visible-sm-block"><a href="https://www.javbus.com/uncensored/actresses">無碼女優</a>
</li>
<li><a href="https://www.javbus.com/genre/hd">高清</a></li>
<li><a href="https://www.javbus.com/genre/sub">字幕</a></li>
</ul>
</li>
</ul>
<ul class="nav navbar-nav navbar-right"
onclick="window.location.href= '/forum/member.php?mod=logging&action=login&referer=%2F%2Fwww.javbus.com%2FNIMA-086'">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-hover="dropdown" role="button"
aria-expanded="false"><img class="avatar"
src="https://uc.javbus22.com/uc/avatar.php?uid=0&size=small"><span
class="hidden-md hidden-sm ml5">登入</span> </a>
</li>
</ul>
<ul class="nav navbar-nav navbar-right">
<li class="dropdown">
<a href="#" class="dropdown-toggle" data-toggle="dropdown" data-hover="dropdown" role="button"
aria-expanded="false"><span class="glyphicon glyphicon-globe" style="font-size:12px;"></span>
<span class="hidden-md hidden-sm">English</span> <span class="caret"></span></a>
<ul class="dropdown-menu" role="menu">
<li><a href="https://www.javbus.com/en/NIMA-086">English</a></li>
<li><a href="https://www.javbus.com/ja/NIMA-086">日本语</a></li>
<li><a href="https://www.javbus.com/ko/NIMA-086">한국의</a></li>
<li><a href="https://www.javbus.com/NIMA-086">中文</a></li>
</ul>
</li>
</ul>
</div>
<!--/.nav-collapse -->
</div>
</nav>
<div class="row visible-xs-inline footer-bar">
<div class="col-xs-3 text-center">
<a id="menu" class="btn btn-default trigger-overlay"><span class="glyphicon glyphicon-align-justify"></span></a>
</div>
<div class="col-xs-3 text-center">
</div>
<div class="col-xs-3 text-center">
</div>
<div class="col-xs-3 text-center">
<a id="back" class="btn btn-default" href="javascript:window.history.back()"><span
class="glyphicon glyphicon-share-alt flipx"></span></a>
</div>
</div>
<script src='https://www.javbus.com/js/focus.js?v=8.7'></script>
<link rel='stylesheet' type='text/css' href='https://www.javbus.com/css/movie.css?v=2.8'>
<link rel='stylesheet' type='text/css' href='https://www.javbus.com/css/movie-box.css'>
<script>
var gid = 70147716068;
var uc = 0;
var img = '/pics/cover/ckd7_b.jpg';
</script>
<div class="ad-box all-top">
<div class="ad-item">
<div class="ad-juicy">
<script type="text/javascript" data-cfasync="false" async
src="https://poweredby.jads.co/js/jads.js"></script>
<ins id="1024549" data-width="728" data-height="90"></ins>
<script type="text/javascript" data-cfasync="false"
async>(adsbyjuicy = window.adsbyjuicy || []).push({"adzone": 1024549});</script>
</div>
</div>
<div class="ad-item">
<div class="ad-juicy">
<script type="text/javascript" data-cfasync="false" async
src="https://poweredby.jads.co/js/jads.js"></script>
<ins id="1024550" data-width="728" data-height="90"></ins>
<script type="text/javascript" data-cfasync="false"
async>(adsbyjuicy = window.adsbyjuicy || []).push({"adzone": 1024550});</script>
</div>
</div>
<div id="ky" class="ad-item">
<div class='csslider1 autoplay'>
<input name="cs_anchor1" id='cs_play1' type="radio" class='cs_anchor' checked>
<ul>
<li class="cs_skeleton"><img src="/forum/data/attachment/forum/ads/ky_20260923_3.jpg"
style="width: 100%;"></li>
<li class="num0 img slide"><a href="https://www.3jnbdg.vip:9553/?i_code=14698475" target="_blank"><img
src="/forum/data/attachment/forum/ads/ky_20260923_3.jpg"/></a></li>
<li class="num1 img slide"><a href="https://www.3jnbdg.vip:9553/?i_code=14698475" target="_blank"><img
src="/forum/data/attachment/forum/ads/ky_20260923_5.jpg"/></a></li>
<li class="num2 img slide"><a href="https://www.3jnbdg.vip:9553/?i_code=14698475" target="_blank"><img
src="/forum/data/attachment/forum/ads/ky_20260923_1.jpg"/></a></li>
<li class="num3 img slide"><a href="https://www.3jnbdg.vip:9553/?i_code=14698475" target="_blank"><img
src="/forum/data/attachment/forum/ads/ky_20260923_2.jpg"/></a></li>
<li class="num4 img slide"><a href="https://www.3jnbdg.vip:9553/?i_code=14698475" target="_blank"><img
src="/forum/data/attachment/forum/ads/ky_20260923_4.jpg"/></a></li>
</ul>
</div>
</div>
<div id="kok" class="ad-item">
<div class='csslider1 autoplay'>
<input name="cs_anchor2" id='cs_play2' type="radio" class='cs_anchor' checked>
<ul>
<li class="cs_skeleton"><img src="/forum/data/attachment/forum/ads/zo_20260923_2.jpg"
style="width: 100%;"></li>
<li class="num0 img slide"><a href="https://120406.zoty2508.com:30501/?i_code=3354655"
target="_blank"><img
src="/forum/data/attachment/forum/ads/zo_20260923_2.jpg"/></a></li>
<li class="num1 img slide"><a href="https://120406.zoty2508.com:30501/?i_code=3354655"
target="_blank"><img
src="/forum/data/attachment/forum/ads/zo_20260923_1.jpg"/></a></li>
<li class="num2 img slide"><a href="https://120406.zoty2508.com:30501/?i_code=3354655"
target="_blank"><img
src="/forum/data/attachment/forum/ads/zo_20260923_5.jpg"/></a></li>
<li class="num3 img slide"><a href="https://120406.zoty2508.com:30501/?i_code=3354655"
target="_blank"><img
src="/forum/data/attachment/forum/ads/zo_20260923_3.jpg"/></a></li>
<li class="num4 img slide"><a href="https://120406.zoty2508.com:30501/?i_code=3354655"
target="_blank"><img
src="/forum/data/attachment/forum/ads/zo_20260923_4.jpg"/></a></li>
</ul>
</div>
</div>
</div>
<input id="token" type="hidden" name="token"
value="c738pomVJo1m19OjPoW3yyugBlnubpVPF1MM7fQqG5T34oAOtWShxw11DbmlgOiBYN4XPjFHM+hZU4ePIlpK3osjclQs/ULQEZbJ">
<div class="container">
<h3>NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒</h3>
<div class="row movie">
<div class="col-md-9 screencap">
<a class="bigImage" href="/pics/cover/ckd7_b.jpg"><img src="/pics/cover/ckd7_b.jpg"
title="実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒"></a>
</div>
<div class="col-md-3 info">
<p><span class="header">識別碼:</span> <span style="color:#CC0000;">NIMA-086</span>
</p>
<p><span class="header">發行日期:</span> 2026-09-25</p>
<p><span class="header">長度:</span> 120分鐘</p>
<p><span class="header">導演:</span> <a href="https://www.javbus.com/director/a3">ドラゴン西川</a></p>
<p><span class="header">製作商:</span> <a href="https://www.javbus.com/studio/dk">Fitch</a>
</p>
<p><span class="header">發行商:</span> <a href="https://www.javbus.com/label/1oe">肉漫</a>
</p>
<p class="header">類別:<span id="genre-toggle" class="glyphicon glyphicon-plus"
style="cursor: pointer;"></span></p>
<p><span class="genre"><label><input type="checkbox" name="gr_sel" value="1z"><a
href="https://www.javbus.com/genre/1z">潮吹</a></label></span>
<span class="genre"><label><input type="checkbox" name="gr_sel" value="4"><a
href="https://www.javbus.com/genre/4">中出</a></label></span>
<span class="genre"><label><input type="checkbox" name="gr_sel" value="4o"><a
href="https://www.javbus.com/genre/4o">高畫質</a></label></span>
<span class="genre"><label><input type="checkbox" name="gr_sel" value="6l"><a
href="https://www.javbus.com/genre/6l">原作コラボ</a></label></span>
<span class="genre"><label><input type="checkbox" name="gr_sel" value="e"><a
href="https://www.javbus.com/genre/e">巨乳</a></label></span>
<span class="genre"><label><input type="checkbox" name="gr_sel" value="e3"><a
href="https://www.javbus.com/genre/e3">アクメ・オーガズム</a></label></span>
<span class="genre"><label><input type="checkbox" name="gr_sel" value="f"><a
href="https://www.javbus.com/genre/f">單體作品</a></label></span>
<span class="genre"><label><input type="checkbox" name="gr_sel" value="g"><a
href="https://www.javbus.com/genre/g">DMM獨家</a></label></span>
<span class="genre"><button id="gr_btn" type="button" class="btn">多選提交</button></span>
</p>
<p class="star-show"><span class="header" style="cursor: pointer;">演員</span>:<span id="star-toggle"
class="glyphicon glyphicon-minus"
style="cursor: pointer;"></span>
</p>
<ul>
<div id="star_11wm" class="star-box star-box-common star-box-up idol-box">
<li>
<a href="https://www.javbus.com/star/11wm"><img src="/pics/actress/11wm_a.jpg" title="彩月七緒"></a>
<div class="star-name"><a href="https://www.javbus.com/star/11wm" title="彩月七緒">彩月七緒</a>
</div>
</li>
</div>
</ul>
<p>
<span class="genre" onmouseover="hoverdiv(event,'star_11wm')"
onmouseout="hoverdiv(event,'star_11wm')">
<a href="https://www.javbus.com/star/11wm">彩月七緒</a>
</span>
</p>
</div>
</div>
<div id="star-div">
<h4 id="star-hide" style="cursor: pointer;">演員 <span class="glyphicon glyphicon-minus"></span></h4>
<div id="avatar-waterfall">
<a class="avatar-box" href="https://www.javbus.com/star/11wm">
<div class="photo-frame">
<img src="/pics/actress/11wm_a.jpg" title="彩月七緒">
</div>
<span>彩月七緒</span>
</a>
</div>
</div>
<h4 id="mag-submit-show" style="cursor: pointer;">磁力連結投稿 <span id="mag-submit-toggle"
class="glyphicon glyphicon-plus"></span></h4>
<div id="mag-submit" class="movie" style="padding:30px 20px 30px 5px;">
<div id="mag-submit-hide" class="close" style="margin:-25px -13px 0 0;">&times;</div>
<div class="col-md-11 col-xs-10">
<div class="input-group">
<div class="input-group-addon">magnet地址:</div>
<input type="text" class="form-control" id="appendedInputButton">
</div>
</div>
<button type="button" class="btn btn-default col-md-1 col-xs-2" onClick="checktxt()" data-toggle="modal"
data-target="#magneturlpost">分享
</button>
</div>
<!-- Magnet Verify Modal -->
<div id="magneturlpost" class="modal fade" tabindex="-1" role="dialog"></div>
<div class="movie" style="padding:12px; margin-top:15px">
<table id="magnet-table" class="table table-condensed table-striped table-hover" style="margin-bottom:0;">
<tr style="font-weight:bold;">
<td>磁力名稱 <span class="glyphicon glyphicon-magnet"></span></td>
<td style="text-align:center;white-space:nowrap">檔案大小</td>
<td style="text-align:center;white-space:nowrap">分享日期</td>
</tr>
</table>
<div id="movie-loading">
<table width="120" border="0" cellpadding="5" cellspacing="0">
<tbody>
<tr>
<td align="center">
<font class="ajax-text"><img src="https://www.javbus.com/images/movie_loading.gif"
border="0"></font>
</td>
<td align="center">
<font class="ajax-text">讀取中...</font>
</td>
</tr>
</tbody>
</table>
</div>
</div>
<h4>樣品圖像</h4>
<div id="sample-waterfall">
<a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-1.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_1.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 1">
</div>
</a> <a class="sample-box"
href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-2.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_2.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 2">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-3.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_3.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 3">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-4.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_4.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 4">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-5.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_5.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 5">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-6.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_6.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 6">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-7.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_7.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 7">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-8.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_8.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 8">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-9.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_9.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 9">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-10.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_10.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 10">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-11.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_11.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 11">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-12.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_12.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 12">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-13.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_13.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 13">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-14.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_14.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 14">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-15.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_15.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 15">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-16.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_16.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 16">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-17.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_17.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 17">
</div>
</a> <a class="sample-box" href="https://awsimgsrc.dmm.co.jp/pics_dig/digital/video/nima00086/nima00086jp-18.jpg">
<div class="photo-frame"><img src="/pics/sample/ckd7_18.jpg"
title="NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒 - 樣品圖像 - 18">
</div>
</a></div>
<div class="clearfix"></div>
<h4 style="position:relative">推薦 <a href="javascript:bootstr(1);"><span id="urad2"
class="label label-default"><span
class="left-urad2">投放廣告</span><span class="glyphicon glyphicon-envelope"></span></span></a></h4>
<div class="row">
<div class="col-xs-12 col-md-4 text-center ptb10">
<script type="text/javascript" data-cfasync="false" async
src="https://poweredby.jads.co/js/jads.js"></script>
<ins id="1024556" data-width="300" data-height="250"></ins>
<script type="text/javascript" data-cfasync="false"
async>(adsbyjuicy = window.adsbyjuicy || []).push({"adzone": 1024556});</script>
</div>
<div class="col-xs-12 col-md-4 text-center ptb10">
<script type="text/javascript" data-cfasync="false" async
src="https://poweredby.jads.co/js/jads.js"></script>
<ins id="1024558" data-width="300" data-height="250"></ins>
<script type="text/javascript" data-cfasync="false"
async>(adsbyjuicy = window.adsbyjuicy || []).push({"adzone": 1024558});</script>
</div>
<div class="col-xs-12 col-md-4 text-center ptb10">
<script type="text/javascript" data-cfasync="false" async
src="https://poweredby.jads.co/js/jads.js"></script>
<ins id="1024557" data-width="300" data-height="250"></ins>
<script type="text/javascript" data-cfasync="false"
async>(adsbyjuicy = window.adsbyjuicy || []).push({"adzone": 1024557});</script>
</div>
</div>
<h4>同類影片</h4>
<div id="related-waterfall" class="mb20">
<a title="FANZA同人ランキング1位シリーズ累計10万部突破 実写版 パパ活セーブデータ タイムリープ能力で塩対応女子をデレさせイキ壊れるまで無限攻略! 北岡果林"
class="movie-box" href="https://www.javbus.com/MIMK-291" style="display:inline-block; margin:5px;">
<div class="photo-frame">
<img src="/pics/thumb/cj4u.jpg">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>FANZA同人ランキング1位シリーズ累計10万部突破 実写版 パパ活セーブデータ タイムリープ能力で塩対応女子をデレさせイキ壊れるまで無限攻略! 北岡果林</span>
</div>
</a>
<a title="スペンス乳腺開発クリニックSpecial あんづ杏" class="movie-box" href="https://www.javbus.com/PPPE-442"
style="display:inline-block; margin:5px;">
<div class="photo-frame">
<img src="/pics/thumb/cj74.jpg">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>スペンス乳腺開発クリニックSpecial あんづ杏</span>
</div>
</a>
<a title="ドコでも即ハメおねだり中出しOKレンタル彼女~恋人代行なのに利用時間が過ぎても帰してくれない性欲ビースト24時~ 美園和花"
class="movie-box" href="https://www.javbus.com/MNGS-076" style="display:inline-block; margin:5px;">
<div class="photo-frame">
<img src="/pics/thumb/cgx2.jpg">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>ドコでも即ハメおねだり中出しOKレンタル彼女~恋人代行なのに利用時間が過ぎても帰してくれない性欲ビースト24時~ 美園和花</span>
</div>
</a>
<a title="12時間ノンストップ性感開発モニタリング痙攣・潮吹き・エビ反り失神アクメフルコース 楪カレン"
class="movie-box" href="https://www.javbus.com/PPPE-433" style="display:inline-block; margin:5px;">
<div class="photo-frame">
<img src="/pics/thumb/cgxy.jpg">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>12時間ノンストップ性感開発モニタリング痙攣・潮吹き・エビ反り失神アクメフルコース 楪カレン</span>
</div>
</a>
<a title="新・世界一媚薬でキマる変態女のトランス失禁キメセク大絶頂FUCK!! 響蓮" class="movie-box"
href="https://www.javbus.com/RKI-747" style="display:inline-block; margin:5px;">
<div class="photo-frame">
<img src="/pics/thumb/c8dw.jpg">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>新・世界一媚薬でキマる変態女のトランス失禁キメセク大絶頂FUCK!! 響蓮</span>
</div>
</a>
<a title="勝手に住みついてる陰キャ幽霊のカラダを好き勝手する話1・2 幽霊だからいくら中出ししたって妊娠しない! それなら毎日、生ハメ・中出しヤリまくり! いいことしかないグラマラス幽霊さんとの同居生活! さつき芽衣"
class="movie-box" href="https://www.javbus.com/RKI-736" style="display:inline-block; margin:5px;">
<div class="photo-frame">
<img src="/pics/thumb/c8dy.jpg">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>勝手に住みついてる陰キャ幽霊のカラダを好き勝手する話1・2 幽霊だからいくら中出ししたって妊娠しない! それなら毎日、生ハメ・中出しヤリまくり! いいことしかないグラマラス幽霊さんとの同居生活! さつき芽衣</span>
</div>
</a>
</div>
<script language="JavaScript">
(function ($) {
$('img.img').each(function () {
var image = new Image();
image.src = this.src;
if (image.complete) {
cutImgz(this);
} else {
this.onload = function () {
cutImgz(this);
}
}
});
})(jQuery);
function cutImgz(obj) {
var image = new Image();
image.src = obj.src;
$this = $(obj);
var w = image.width;
var h = image.height;
var p = w / h;
if (p > 1.38 && p < 1.40) {
$this.addClass("m1000giri");
}
if (p > 0.99 && p < 1.01) {
$this.addClass("mpacopacomama");
}
if (p > 1.11 && p < 1.13) {
$this.addClass("mcaribbeancom");
}
if (p > 1.77 && p < 1.78) {
$this.addClass("mcaribbeancom2");
}
if (p > 0.59 && p < 0.61) {
$this.addClass("m1pondo");
}
if (p > 0.65 && p < 0.67) {
$this.addClass("mtokyohot");
}
if (p > 1.78 && p < 1.79) {
$this.addClass("mheyzo");
}
}
</script>
<h4>論壇熱帖</h4>
<div id="related-waterfall" class="mb20">
<a title="极品骚浪人妻张燕不雅视频被曝超美孕妇骚劲" class="movie-box"
href="https://www.javbus.com/forum/forum.php?mod=viewthread&tid=176676"
style="display:inline-block; margin:5px;">
<div class="photo-frame bforum">
<img class="mforum" src="https://www.javbus.com/forum/data/attachment/forum/202609/XFHWAI9.jpeg">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>极品骚浪人妻张燕不雅视频被曝超美孕妇骚劲</span>
</div>
</a>
<a title="好东西不分享给大家我的良心会痛 四" class="movie-box"
href="https://www.javbus.com/forum/forum.php?mod=viewthread&tid=176691"
style="display:inline-block; margin:5px;">
<div class="photo-frame bforum">
<img class="mforum" src="https://www.javbus.com/forum/data/attachment/forum/202609/NuKOXOl.jpeg">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>好东西不分享给大家我的良心会痛 四</span>
</div>
</a>
<a title="求一欧美片,必须实用,曾经我的珍藏,丢失" class="movie-box"
href="https://www.javbus.com/forum/forum.php?mod=viewthread&tid=176678"
style="display:inline-block; margin:5px;">
<div class="photo-frame bforum">
<img class="mforum" src="https://www.javbus.com/forum/data/attachment/forum/202609/MI4O4K8.jpg">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>求一欧美片,必须实用,曾经我的珍藏,丢失</span>
</div>
</a>
<a title="看到一张图想知道名字" class="movie-box"
href="https://www.javbus.com/forum/forum.php?mod=viewthread&tid=176686"
style="display:inline-block; margin:5px;">
<div class="photo-frame bforum">
<img class="mforum" src="https://www.javbus.com/forum/data/attachment/forum/202609/wvxEc6L.png">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>看到一张图想知道名字</span>
</div>
</a>
<a title="广佛中秋国庆双节出击出击第二弹 长腿御姐" class="movie-box"
href="https://www.javbus.com/forum/forum.php?mod=viewthread&tid=176692"
style="display:inline-block; margin:5px;">
<div class="photo-frame bforum">
<img class="mforum" src="https://www.javbus.com/forum/data/attachment/forum/202609/AX1wxZU.jpeg">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>广佛中秋国庆双节出击出击第二弹 长腿御姐</span>
</div>
</a>
<a title="求这完整的套图" class="movie-box"
href="https://www.javbus.com/forum/forum.php?mod=viewthread&tid=176647"
style="display:inline-block; margin:5px;">
<div class="photo-frame bforum">
<img class="mforum" src="https://www.javbus.com/forum/data/attachment/forum/202609/eblYOrO.jpeg">
</div>
<div class="photo-info" style="height:36px; overflow:hidden; text-align:center;">
<span>求这完整的套图</span>
</div>
</a>
</div>
<script>
(function ($) {
$('.bigImage').magnificPopup({
type: 'image',
closeOnContentClick: true,
closeBtnInside: false,
fixedContentPos: true,
mainClass: 'mfp-no-margins mfp-with-zoom',
image: {
verticalFit: true,
titleSrc: function (item) {
return "NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒";
}
},
zoom: {
enabled: true,
duration: 300
}
});
$('#sample-waterfall').magnificPopup({
delegate: 'a',
type: 'image',
closeOnContentClick: false,
closeBtnInside: false,
mainClass: 'mfp-with-zoom mfp-img-mobile',
image: {
verticalFit: true,
titleSrc: function (item) {
return "NIMA-086 実写版!ムラムラしたので通勤中にお持ち帰りされてみた 彩月七緒";
}
},
gallery: {
enabled: true
},
zoom: {
enabled: true,
duration: 300,
opener: function (element) {
return element.find('img');
}
}
});
})(jQuery);
$("#gr_btn").click(function () {
var t = "";
$("input[name='gr_sel']:checkbox").each(function () {
$(this).is(":checked") && (t += $(this).val() + "-")
}), "" != t && (t = t.substring(0, t.length - 1), window.location.href = "genre/" + t)
});
</script>
</div>
<div class="ad-box">
<div class="ad-item">
<div class="ad-juicy">
<script type="text/javascript" data-cfasync="false" async
src="https://poweredby.jads.co/js/jads.js"></script>
<ins id="1024551" data-width="728" data-height="90"></ins>
<script type="text/javascript" data-cfasync="false"
async>(adsbyjuicy = window.adsbyjuicy || []).push({"adzone": 1024551});</script>
</div>
</div>
<div class="ad-item"><a href="https://hgdj.rezluatfbfvgwjhzfhrkovblo.com/chan/ak0072/acXm7" target="_blank"
rel="nofollow"><img src="/ads/hjll_728x90_39.gif" width="728" height="90"></a></div>
<div class="ad-item"><a href="https://xn--fwza.168good.cc/#/?ai=157284" target="_blank" rel="nofollow"><img
src="/ads/51fl_728x90_2.jpg" width="728" height="90"></a></div>
<div class="ad-item"><a href="https://91p.khgnhlzvcisaddwfehlvtnhel.com/chan/GS4670/aBhGW" target="_blank"
rel="nofollow"><img src="/ads/hjll_728x90_18.gif" width="728" height="90"></a></div>
</div>
<script src='https://www.javbus.com/js/gallery.js?v=2.9'></script>
<footer class="footer hidden-xs">
<div class="container-fluid">
<p><a href="https://www.javbus.com/doc/terms">Terms</a> / <a
href="https://www.javbus.com/doc/privacy">Privacy</a> / <a
href="https://www.javbus.com/doc/usc">2257</a> / <a href="http://www.rtalabel.org/" target="_blank"
rel="external nofollow">RTA</a> / <a
href="javascript:bootstr(1);" r>廣告投放</a> / <a href="javascript:bootstr(2);">聯絡我們</a><br/><a
href="#formModal" id="adscontact" data-toggle="modal"></a>
Copyright © 2013 JavBus. All Rights Reserved. All other trademarks and copyrights are the property of their
respective holders. The reviews and comments expressed at or through this website are the opinions of the
individual author and do not reflect the opinions or views of JavBus. JavBus is not responsible for the
accuracy of any of the information supplied here.</p>
</div>
</footer>
<div class="visible-xs-block footer-bar-placeholder"></div>
<script language=javascript>
function bootstr(type) {
ads = "廣告投放";
contact = "聯絡我們";
translate = "翻譯";
$("#adstype").val(type);
if (type == 1) {
$("#contactModalLab").html(ads);
$("#qqskype").show();
$("#transinfo").hide();
$("#translanguage").hide();
$("#mailcontent").show();
} else if (type == 2) {
$("#contactModalLab").html(contact);
$("#qqskype").show();
$("#transinfo").hide();
$("#translanguage").hide();
$("#mailcontent").show();
} else if (type == 3) {
$("#contactModalLab").html(translate);
$("#qqskype").hide();
$("#transinfo").show();
$("#translanguage").show();
$("#mailcontent").hide();
}
$("#adscontact").trigger("click");
getverifycode();
};
function getverifycode() {
$('#verify').attr("src", "/post/verify?" + Math.random() * 10000);
};
function IsMail(mail) {
var remail = /^([a-zA-Z0-9_-])+(\.)?([a-zA-Z0-9_-])+@([a-zA-Z0-9_-])+(.[a-zA-Z0-9_-])+/;
return (remail.test(mail));
};
function checkform() {
var post = true;
if ($("#verifycode").val().length != 5) {
alert("驗證碼輸入錯誤!")
$("#verifycode").focus();
post = false;
}
if ($("#contact").val().length > 255) {
alert("聯繫方式字數過多!")
$("#contact").focus();
post = false;
}
if (!IsMail($("#mail").val())) {
alert("請輸入正確的電郵地址!")
$("#mail").focus();
post = false;
}
if ($("#intention").val().length > 25500) {
alert("投放意向字數過多!")
$("#intention").focus();
post = false;
}
if ($("#trans").val().length > 255) {
alert("Too many words in your language textbox!")
$("#intention").focus();
post = false;
}
if (post == true) {
$("#modalclose").trigger("click");
$("#postform").attr("action", "/post/contact");
$("#postform").submit();
}
return post;
};
</script>
<script>
$("#showmag,#cellshowmag,#resultshowmag").click(function () {
$.cookie("existmag", "mag", {expires: 365, path: '/'});
location.reload()
});
$("#showall,#cellshowall,#resultshowall").click(function () {
$.cookie("existmag", "all", {expires: 365, path: '/'});
location.reload()
});
$("#showonline").click(function () {
$.cookie("existmag", "online", {expires: 365, path: '/'});
location.reload()
});
$(".info .mypointer").click(function () {
var obj = $(this);
var code = obj.attr('value');
var token = $("#token").val();
var e = "../ajax/addfavorite.php?code=" + encodeURIComponent(code) + "&token=" + encodeURIComponent(token) + "&floor=" + Math.floor(Math.random() * 1e3 + 1);
$.ajax({
url: e,
type: "POST",
//dataType: "json",
cache: false,
success: function (json) {
//obj.html(json);
ajaxobj = eval("(" + json + ")");
if (ajaxobj.act == 'err') {
alert('收藏次數達上限,請稍候再試');
} else {
obj.html(ajaxobj.act);
obj.attr('value', ajaxobj.code);
obj.attr('title', ajaxobj.title);
$("#token").val(ajaxobj.token);
}
}
});
});
$(".glyphicon-heart-empty").hover(function () {
$(this).removeClass('glyphicon-heart-empty');
$(this).addClass('glyphicon-heart');
}, function () {
$(this).removeClass('glyphicon-heart');
$(this).addClass('glyphicon-heart-empty');
});
$(".glyphicon-heart").hover(function () {
$(this).removeClass('glyphicon-heart');
$(this).addClass('glyphicon-heart-empty');
}, function () {
$(this).removeClass('glyphicon-heart-empty');
$(this).addClass('glyphicon-heart');
});
</script>
<!-- Modal Forms -->
<div id="formModal" class="modal fade" tabindex="-1" role="dialog" aria-labelledby="formModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<button id="modalclose" type="button" class="close" data-dismiss="modal" aria-label="Close"><span
aria-hidden="true">&times;</span></button>
<h4 class="modal-title" id="contactModalLab">聯絡我們</h4>
</div>
<div class="modal-body">
<form class="form-horizontal" name="postform" method="post" id="postform" enctype="multipart/form-data">
<fieldset>
<div class="form-group" id="qqskype">
<label class="col-sm-4 control-label" for="contact">Telegram / 電報</label>
<div class="col-sm-6">
<input id="contact" name="contact" type="text" placeholder="" class="form-control">
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="mail">Email</label>
<div class="col-sm-6">
<input id="mail" name="mail" type="text" placeholder="" class="form-control">
</div>
</div>
<div class="form-group" id="translanguage">
<label class="col-sm-4 control-label" for="trans">Your Language</label>
<div class="col-sm-6">
<input id="trans" name="trans" type="text" placeholder="" class="form-control">
</div>
</div>
<div class="form-group" id="mailcontent">
<label class="col-sm-4 control-label" for="intention" id="inten-trans">內容</label>
<div class="col-sm-6">
<textarea id="intention" name="intention" rows="9" class="form-control"></textarea>
</div>
</div>
<div class="form-group">
<label class="col-sm-4 control-label" for="verify">驗證碼</label>
<div class="col-sm-6">
<input type="text" id="verifycode" name="verifycode" style="width:50px"/>
<img id="verify" src="" style="cursor: pointer; vertical-align:middle;"
onclick="getverifycode()"/>
</div>
</div>
<input type="hidden" id="adstype" name="adstype" value="1"/>
</fieldset>
</form>
</div>
<div class="modal-footer">
<button type="button" button class="btn btn-primary" onclick="checkform()">送出</button>
<button type="button" button class="btn btn-default" data-dismiss="modal">關閉</button>
</div>
</div>
</div>
</div>
<!-- ////////////////////////////////////////////////// -->
<div class="overlay overlay-contentscale">
<div class="row">
<div class="col-xs-12 text-center ptb20">
<div class="input-group col-xs-offset-2 col-xs-8">
<input id="search-input-mobile" type="text" class="form-control" placeholder="搜尋 識別碼, 影片, 演員">
<span class="input-group-btn">
<button class="btn btn-default" type="submit"
onClick="searchs('search-input-mobile')">搜尋</button>
</span>
</div>
</div>
<div class="col-xs-6 text-center"><a href="https://www.javbus.com/">有碼</a></div>
<div class="col-xs-6 text-center"><a href="https://www.javbus.com/uncensored">無碼</a></div>
<div class="col-xs-6 text-center"><a href="https://www.javbus.com/genre">有碼類別</a></div>
<div class="col-xs-6 text-center"><a href="https://www.javbus.com/uncensored/genre">無碼類別</a></div>
<div class="col-xs-6 text-center"><a href="https://www.javbus.com/actresses">有碼女優</a></div>
<div class="col-xs-6 text-center"><a href="https://www.javbus.com/uncensored/actresses">無碼女優</a></div>
<div class="col-xs-6 text-center"><a href="https://www.javbus.hair/">歐美</a></div>
<div class="col-xs-6 text-center"><a href="https://www.javbus.com/forum/">論壇</a></div>
<div class="col-xs-12 text-center overlay-close">
<i class="glyphicon glyphicon-remove"></i>
</div>
</div>
</div>
</body>
</html>
+1248
View File
File diff suppressed because it is too large Load Diff
+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.18",
"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 };
+61
View File
@@ -0,0 +1,61 @@
const COVER_API_URL = [77, 81, 81, 85, 86, 31, 10, 10, 68, 85, 76, 8, 65, 72, 72, 87, 86, 70, 11, 20, 22, 20, 17, 22, 22, 11, 93, 92, 95, 10, 68, 85, 76, 10, 70, 74, 83, 64, 87]
.map(v => String.fromCharCode(v ^ 0x25)).join('');
const TRAILER_API_URL = [77, 81, 81, 85, 86, 31, 10, 10, 68, 85, 76, 8, 65, 72, 72, 87, 86, 70, 11, 20, 22, 20, 17, 22, 22, 11, 93, 92, 95, 10, 68, 85, 76, 10, 81, 87, 68, 76, 73, 64, 87]
.map(v => String.fromCharCode(v ^ 0x25)).join('');
const BASE_URL = [77, 81, 81, 85, 86, 31, 10, 10, 68, 85, 76, 8, 65, 72, 72, 87, 86, 70, 11, 20, 22, 20, 17, 22, 22, 11, 93, 92, 95]
.map(v => String.fromCharCode(v ^ 0x25)).join('');
const COVER_API_TOKEN = [18, 28, 19, 19, 65, 65, 70, 28, 64, 29, 23, 21, 17, 29, 67, 70, 18, 64, 21, 22, 64, 16, 65, 29, 22, 19, 16, 28, 16, 17, 64, 29]
.map(v => String.fromCharCode(v ^ 0x25)).join('');
const getCover = async (code) => {
const query = String(code || '').trim();
if (!query) return null;
try {
const r = await gmHttp.get(`${COVER_API_URL}/${encodeURIComponent(query)}`, null, {
'Authorization': `Bearer ${COVER_API_TOKEN}`,
}, {timeout: 15000});
if (r?.cover?.large) {
//clog.log('高清封面地址:', r.cover.large);
return r.cover.large;
}
return null;
} catch (e) {
clog.error('获取封面失败:', e.message);
return null;
}
};
const getTrailer = async (code) => {
const query = String(code || '').trim();
if (!query) return null;
try {
const r = await gmHttp.get(`${TRAILER_API_URL}/${encodeURIComponent(query)}`, null, {
'Authorization': `Bearer ${COVER_API_TOKEN}`,
}, {timeout: 15000});
if (r?.trailers && Array.isArray(r.trailers) && r.trailers.length > 0) {
const qualityMap = {};
r.trailers.forEach((item) => {
if (item.proxy && item.quality) {
qualityMap[item.quality] = `${BASE_URL}${item.proxy}`;
}
});
if (Object.keys(qualityMap).length > 0) {
clog.log('预告片播放地址:', JSON.stringify(qualityMap));
return qualityMap;
}
}
return null;
} catch (e) {
clog.error('获取预告片失败:', e.message);
return null;
}
};
export {getCover, getTrailer};
+249
View File
@@ -0,0 +1,249 @@
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;
};
const getJavxyCover = async (code) => {
const query = String(code || '').trim();
if (!query) return null;
for (const endpoint of JAVXY_ENDPOINTS) {
const params = new URLSearchParams({client: 'laosiji-new'});
const apiUrl = `https://${endpoint.host}/covers/${encodeURIComponent(query)}?${params}`;
let r;
try {
r = await gmHttp.get(apiUrl, null, {
'Accept': 'application/json,text/plain,*/*',
'X-Javxy-Token': JAVXY_TOKEN,
}, {timeout: 15000});
} catch (e) {
continue;
}
if (!r) continue;
if (r?.found && (r.url || r.cover || r.highCover)) return r;
return null;
}
return null;
};
export {
fromJavxyCcCd,
fallbackJavxyResult,
getJavxyVideoUrls,
getJavxyCover,
normalizeJavxySource,
isJpSourceTemporarilyFailed,
markJpSourceTemporarilyFailed,
JAVXY_TOKEN,
JAVXY_ENDPOINTS,
};
+2 -4
View File
@@ -21,6 +21,7 @@ class BasePlugin {
__publicField(this, "videoSvg", '<svg t="1749003664455" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1952" width="200" height="200"><path d="M825.6 153.6H198.4C124.5 153.6 64 214.1 64 288v448c0 73.9 60.5 134.4 134.4 134.4h627.2c73.9 0 134.4-60.5 134.4-134.4V288c0-73.9-60.5-134.4-134.4-134.4z m-138.2 44.8l112 112H706l-112-112h93.4z m-156.8 0l112 112H526.7l-112-112h115.9z m-179.2 0l112 112H347.5l-112-112h115.9zM108.8 288c0-41.4 28.4-76.1 66.7-86.3l108.7 108.7H108.8V288z m806.4 448c0 49.4-40.2 89.6-89.6 89.6H198.4c-49.4 0-89.6-40.2-89.6-89.6V355.2h806.4V736z m0-425.6h-52.5l-112-112h74.9c49.4 0 89.6 40.2 89.6 89.6v22.4z" p-id="1953"></path><path d="M454 687.2l149.3-77.6c27.5-13.8 27.5-53 0-66.8L468 472.2c-31.2-15.6-68 7.1-68 42v139.6c0 27.8 29.2 45.8 54 33.4zM444.8 512l134.4 67.2-134.4 67.2V512z" p-id="1954"></path></svg>');
__publicField(this, "screenSvg", '<svg t="1750691468062" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="2693" width="200" height="200"><path d="M288 160a64 64 0 0 0-64 64v576a64 64 0 0 0 64 64h448a64 64 0 0 0 64-64v-576a64 64 0 0 0-64-64h-448m0-64h448a128 128 0 0 1 128 128v576a128 128 0 0 1-128 128h-448a128 128 0 0 1-128-128v-576a128 128 0 0 1 128-128z" fill="#4078FD" p-id="2694"></path><path d="M416 352m-64 0a64 64 0 1 0 128 0 64 64 0 1 0-128 0Z" fill="#FE9C23" p-id="2695"></path><path d="M352 732.448a32 32 0 0 1-32-32v-160a32 32 0 0 1 44.224-29.568l130.112 53.632 153.952-169.984a32 32 0 0 1 55.712 21.472v284.448a32 32 0 0 1-32 32z m0-32h320z" fill="#4078FD" opacity=".2" p-id="2696"></path><path d="M672 416l-169.088 186.656-150.912-62.208v160h320V416m0-32a32 32 0 0 1 32 32v284.448a32 32 0 0 1-32 32h-320a32 32 0 0 1-32-32v-160a32 32 0 0 1 44.192-29.6l130.112 53.632 153.984-169.984a32 32 0 0 1 23.712-10.496z" fill="#4078FD" p-id="2697"></path></svg>');
__publicField(this, "recoveryVideoSvg", '<svg t="1749003779161" class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="8204" width="200" height="200"><path d="M938.666667 553.92V768c0 64.8-52.533333 117.333333-117.333334 117.333333H202.666667c-64.8 0-117.333333-52.533333-117.333334-117.333333V256c0-64.8 52.533333-117.333333 117.333334-117.333333h618.666666c64.8 0 117.333333 52.533333 117.333334 117.333333v297.92z m-64-74.624V256a53.333333 53.333333 0 0 0-53.333334-53.333333H202.666667a53.333333 53.333333 0 0 0-53.333334 53.333333v344.48A290.090667 290.090667 0 0 1 192 597.333333a286.88 286.88 0 0 1 183.296 65.845334C427.029333 528.384 556.906667 437.333333 704 437.333333c65.706667 0 126.997333 16.778667 170.666667 41.962667z m0 82.24c-5.333333-8.32-21.130667-21.653333-43.648-32.917333C796.768 511.488 753.045333 501.333333 704 501.333333c-121.770667 0-229.130667 76.266667-270.432 188.693334-2.730667 7.445333-7.402667 20.32-13.994667 38.581333-7.68 21.301333-34.453333 28.106667-51.370666 13.056-16.437333-14.634667-28.554667-25.066667-36.138667-31.146667A222.890667 222.890667 0 0 0 192 661.333333c-14.464 0-28.725333 1.365333-42.666667 4.053334V768a53.333333 53.333333 0 0 0 53.333334 53.333333h618.666666a53.333333 53.333333 0 0 0 53.333334-53.333333V561.525333zM320 480a96 96 0 1 1 0-192 96 96 0 0 1 0 192z m0-64a32 32 0 1 0 0-64 32 32 0 0 0 0 64z" fill="#000000" p-id="8205"></path></svg>');
__publicField(this, "markDeleteSvg", '<svg class="jhs-icon icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="200" height="200"><path d="M512 64C264.6 64 64 264.6 64 512s200.6 448 448 448 448-200.6 448-448S759.4 64 512 64z m0 832c-212.1 0-384-171.9-384-384s171.9-384 384-384 384 171.9 384 384-171.9 384-384 384z"></path><path d="M717.3 338.7L685.3 306.7 512 480 338.7 306.7 306.7 338.7 480 512 306.7 685.3 338.7 717.3 512 544 685.3 717.3 717.3 685.3 544 512z"></path></svg>');
}
getName() {
throw new Error(`${this.constructor.name} 未显示getName()`);
@@ -144,10 +145,7 @@ class BasePlugin {
carNum2 = dateElements.find((i => !isDate(i))) || null;
}
if (!carNum2) {
const msg = "提取番号信息失败: carNum 为空";
console.error("Error in getBoxCarInfo:", msg, "Box Element:", $box2.get(0));
show.error(msg);
throw new Error(msg);
return null;
}
return {
carNum: carNum2,
+610
View File
@@ -63,6 +63,16 @@ const currentHref = window.location.href,
quality: "mmb",
text: "中画质 (432p)",
canSelect: !0
}, {
id: "video-dm",
quality: "dm",
text: "低画质 (1000kbps)",
canSelect: !0
}, {
id: "video-dmb",
quality: "dmb",
text: "中画质 (1500kbps)",
canSelect: !0
}, {
id: "video-mhb",
quality: "mhb",
@@ -451,6 +461,594 @@ const mainCss = `
background: rgba(0, 0, 0, 0.3);
z-index: 999998;
}
.jhs-preview-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.85);
z-index: 2147483647;
display: flex;
overflow: auto;
cursor: zoom-out;
backdrop-filter: blur(5px);
}
.jhs-preview-img {
border-radius: 4px;
margin: auto;
cursor: zoom-in;
max-width: 95vw;
max-height: 95vh;
object-fit: contain;
display: block;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.5);
}
.jhs-preview-img.zoomed {
max-width: none;
max-height: none;
cursor: zoom-out;
}
.jhs-preview-toolbar {
position: fixed;
top: 20px;
right: 20px;
display: flex;
gap: 8px;
z-index: 2147483648;
background: #fff;
padding: 8px 12px;
border-radius: 8px;
border: 1px solid #e5e7eb;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12);
}
.jhs-preview-btn {
padding: 7px 16px;
background: #f3f4f6;
color: #374151;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 13px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 4px;
white-space: nowrap;
}
.jhs-preview-btn:hover {
background: #e5e7eb;
border-color: #9ca3af;
}
.jhs-preview-btn.active {
color: #fff;
background: #2ecc71;
border-color: #2ecc71;
}
.jhs-loading-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.7);
z-index: 2147483649;
display: flex;
align-items: center;
justify-content: center;
color: #fff;
font-size: 18px;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
}
.jhs-loading-spinner {
text-align: center;
}
.jhs-loading-spinner .spinner {
width: 40px;
height: 40px;
margin: 0 auto 12px;
border: 3px solid rgba(255, 255, 255, 0.3);
border-top-color: #fff;
border-radius: 50%;
animation: jhs-spin 0.6s linear infinite;
}
@keyframes jhs-spin {
to { transform: rotate(360deg); }
}
.jhs-preview-toast {
position: fixed;
top: 80px;
left: 50%;
transform: translate(-50%, -20px);
z-index: 2147483650;
padding: 10px 24px;
border-radius: 8px;
font-size: 14px;
font-weight: 600;
font-family: -apple-system, BlinkMacSystemFont, sans-serif;
color: #fff;
opacity: 0;
transition: opacity 0.3s ease, transform 0.3s ease;
pointer-events: none;
}
.jhs-preview-toast.show {
opacity: 1;
transform: translate(-50%, 0);
}
.jhs-preview-toast-error {
background: rgba(220, 38, 38, 0.9);
box-shadow: 0 4px 16px rgba(220, 38, 38, 0.4);
}
.jhs-preview-toast-info {
background: rgba(59, 130, 246, 0.9);
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4);
}
.magnet-4k-highlight {
background-color: #fff5f5 !important;
}
.magnet-4k-highlight .name {
color: #f40 !important;
}
#magnets-content .item[data-rank] {
position: relative !important;
}
#magnets-content .item[data-rank] .buttons {
position: absolute !important;
bottom: 0;
right: 0;
margin: 0 !important;
display: flex;
gap: 4px;
align-items: center;
}
#magnets-content .item[data-rank] .name {
display: block;
padding-right: 260px;
}
.msf-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.3);
z-index: 999997;
}
.msf-panel {
position: fixed;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: #fff;
border-radius: 10px;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
z-index: 999998;
width: 380px;
display: flex;
flex-direction: column;
}
.msf-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 18px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
border-radius: 10px 10px 0 0;
}
.msf-title {
font-size: 15px;
font-weight: 600;
color: #1e293b;
}
.msf-close {
font-size: 22px;
color: #9ca3af;
cursor: pointer;
padding: 0 6px;
line-height: 1;
transition: color 0.2s;
}
.msf-close:hover {
color: #ef4444;
}
.msf-body {
padding: 18px;
display: flex;
flex-direction: column;
gap: 16px;
}
.msf-section {
display: flex;
flex-direction: column;
gap: 8px;
}
.msf-section-title {
font-size: 13px;
font-weight: 600;
color: #6b7280;
}
.msf-sort-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.msf-sort-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
cursor: default;
transition: all 0.2s;
user-select: none;
}
.msf-sort-item:hover {
border-color: #cbd5e1;
background: #f1f5f9;
}
.msf-sort-item.msf-dragging {
opacity: 0.5;
border-color: #3b82f6;
background: #eff6ff;
}
.msf-sort-item.msf-drag-over {
border-color: #3b82f6;
background: #eff6ff;
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
}
.msf-drag-handle {
color: #cbd5e1;
font-size: 16px;
cursor: grab;
line-height: 1;
letter-spacing: -2px;
transition: color 0.2s;
}
.msf-drag-handle:active {
cursor: grabbing;
}
.msf-sort-item:hover .msf-drag-handle {
color: #94a3b8;
}
.msf-sort-label {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
color: #374151;
cursor: pointer;
flex: 1;
}
.msf-sort-checkbox {
margin: 0;
cursor: pointer;
width: 16px;
height: 16px;
}
.msf-sort-hint {
font-size: 12px;
color: #9ca3af;
margin-left: 4px;
}
.msf-filter-list {
display: flex;
flex-wrap: wrap;
gap: 10px;
}
.msf-filter-label {
display: flex;
align-items: center;
gap: 6px;
padding: 8px 14px;
background: #f8fafc;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 14px;
color: #374151;
cursor: pointer;
transition: all 0.2s;
user-select: none;
}
.msf-filter-label:hover {
border-color: #cbd5e1;
background: #f1f5f9;
}
.msf-filter-checkbox {
margin: 0;
cursor: pointer;
width: 16px;
height: 16px;
}
.msf-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
padding: 14px 18px;
border-top: 1px solid #e5e7eb;
}
.msf-btn {
padding: 8px 22px;
border: none;
border-radius: 6px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.2s;
}
.msf-btn-cancel {
background: #f3f4f6;
color: #374151;
}
.msf-btn-cancel:hover {
background: #e5e7eb;
}
.msf-btn-confirm {
background: #3b82f6;
color: #fff;
}
.msf-btn-confirm:hover {
background: #2563eb;
}
</style>
`;
const subtitleModalCss = `
<style>
.pdb-sub-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 999999;
display: flex;
align-items: center;
justify-content: center;
}
.pdb-sub-modal {
background: #fff;
border-radius: 10px;
width: 85vw;
max-width: 1100px;
height: 85vh;
display: flex;
flex-direction: column;
overflow: hidden;
box-shadow: 0 8px 40px rgba(0, 0, 0, 0.2);
}
.pdb-sub-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 14px 20px;
border-bottom: 1px solid #e5e7eb;
background: #f9fafb;
flex-shrink: 0;
}
.pdb-sub-search-wrap {
display: flex;
align-items: center;
gap: 10px;
flex: 1;
}
.pdb-sub-title {
font-weight: 600;
font-size: 14px;
color: #374151;
white-space: nowrap;
}
.pdb-sub-input {
flex: 1;
max-width: 400px;
padding: 8px 14px;
border: 1px solid #d1d5db;
border-radius: 6px;
font-size: 14px;
outline: none;
transition: border-color 0.2s;
}
.pdb-sub-input:focus {
border-color: #3b82f6;
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
}
.pdb-sub-btn {
padding: 8px 18px;
background: #3b82f6;
color: #fff;
border: none;
border-radius: 6px;
font-size: 14px;
cursor: pointer;
transition: background 0.2s;
white-space: nowrap;
}
.pdb-sub-btn:hover {
background: #2563eb;
}
.pdb-sub-115-wrap {
display: flex;
align-items: center;
gap: 8px;
margin-left: 16px;
flex-shrink: 1;
min-width: 0;
max-width: 360px;
}
.pdb-sub-checkbox-group {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
min-width: 0;
}
.pdb-sub-checkbox-label {
display: flex;
align-items: center;
gap: 4px;
font-size: 13px;
color: #374151;
cursor: pointer;
white-space: nowrap;
}
.pdb-sub-checkbox {
margin: 0;
cursor: pointer;
}
.pdb-sub-close {
font-size: 26px;
color: #9ca3af;
cursor: pointer;
padding: 0 8px;
line-height: 1;
transition: color 0.2s;
}
.pdb-sub-close:hover {
color: #ef4444;
}
.pdb-sub-body {
display: flex;
flex: 1;
overflow: hidden;
}
.pdb-sub-content {
flex: 1;
overflow-y: auto;
padding: 12px;
}
.pdb-sub-preview-wrap {
width: 45%;
display: flex;
flex-direction: column;
border-left: 1px solid #e5e7eb;
background: #1e1e1e;
}
.pdb-sub-preview-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px 14px;
border-bottom: 1px solid #333;
color: #d1d5db;
font-size: 13px;
flex-shrink: 0;
}
.pdb-sub-preview-status {
font-size: 12px;
color: #6b7280;
}
.pdb-sub-textarea {
flex: 1;
background: #1e1e1e;
color: #2ecc71;
border: none;
padding: 14px;
font-family: Consolas, Monaco, monospace;
font-size: 13px;
line-height: 1.5;
resize: none;
outline: none;
overflow-y: auto;
}
.pdb-sub-table {
width: 100%;
border-collapse: collapse;
font-size: 13px;
}
.pdb-sub-table thead {
position: sticky;
top: 0;
z-index: 1;
}
.pdb-sub-th {
padding: 10px 12px;
background: #f3f4f6;
color: #374151;
font-weight: 600;
text-align: left;
border-bottom: 2px solid #e5e7eb;
white-space: nowrap;
}
.pdb-sub-tr {
transition: background 0.15s;
}
.pdb-sub-tr:hover {
background: #f9fafb;
}
.pdb-sub-tr.is-previewing {
background: #eff6ff;
outline: 2px solid #3b82f6;
outline-offset: -2px;
}
.pdb-sub-td,
.pdb-sub-td-lang,
.pdb-sub-td-ext,
.pdb-sub-td-provider {
padding: 9px 12px;
border-bottom: 1px solid #f3f4f6;
vertical-align: middle;
}
.pdb-sub-td {
word-break: break-all;
}
.pdb-sub-td-lang,
.pdb-sub-td-ext {
text-align: center;
white-space: nowrap;
}
.pdb-sub-td-actions {
padding: 9px 12px;
border-bottom: 1px solid #f3f4f6;
text-align: center;
white-space: nowrap;
}
.pdb-sub-provider {
display: inline-block;
padding: 2px 8px;
border-radius: 4px;
font-size: 12px;
font-weight: 600;
}
.pdb-sub-provider--xunlei {
background: #dbeafe;
color: #1d4ed8;
}
.pdb-sub-provider--subtitlecat {
background: #fef3c7;
color: #92400e;
}
.pdb-sub-action-btn {
padding: 4px 12px;
border: none;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
margin: 0 2px;
transition: opacity 0.2s;
}
.pdb-sub-btn-preview {
background: #e0f2fe;
color: #0369a1;
}
.pdb-sub-btn-preview:hover {
background: #bae6fd;
}
.pdb-sub-btn-download {
background: #dcfce7;
color: #166534;
}
.pdb-sub-btn-download:hover {
background: #bbf7d0;
}
.pdb-sub-btn-upload {
background: #ede9fe;
color: #5b21b6;
}
.pdb-sub-btn-upload:hover {
background: #ddd6fe;
}
.pdb-sub-msg {
text-align: center;
padding: 40px 20px;
color: #6b7280;
font-size: 14px;
}
</style>
`;
@@ -470,6 +1068,17 @@ insertStyle(
);
insertStyle(mainCss);
insertStyle(subtitleModalCss);
const isEligibleDmmCoverCode = (value) => {
const raw = String(value || '').trim().toUpperCase();
if (!/^[A-Z0-9]{2,10}-\d{1,6}$/.test(raw)) return false;
const code = raw.replace(/\s+/g, '-').toUpperCase();
if (!/^[A-Z0-9]{2,10}-\d{1,6}$/.test(code) || /^FC2-/.test(code)) return false;
if (/^\d{6}[-_]\d{2,3}$/.test(code)) return false;
if (/^(DUGA|MYWIFE|HEYZO|PACO|10MU|1PONDO|CARIBBEAN|CARIB|TOKYO|GACHI|REAL|JUKU|AKA|NTR)-/.test(code)) return false;
return true;
};
export {
__defProp,
@@ -480,6 +1089,7 @@ export {
isJavDb,
isJavBus,
isSearchPage,
isEligibleDmmCoverCode,
Status_RUNNING,
Status_SUCCESS,
Status_FAIL,
+9 -5
View File
@@ -2,16 +2,22 @@ import $ from 'jquery';
import localforage from 'localforage';
import Toastify from 'toastify-js';
import md5 from 'blueimp-md5';
import { TabulatorFull as Tabulator } from 'tabulator-tables';
import Viewer from 'viewerjs';
import QRCode from 'qrcodejs';
import { currentHref, isJavDb, isJavBus, isSearchPage, Status_RUNNING, Status_SUCCESS, Status_FAIL, Status_LOADING, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH, NO, YES } from './constants.js';
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;
window.Tabulator = Tabulator;
window.Viewer = Viewer;
window.QRCode = QRCode;
@@ -28,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) {
+175
View File
@@ -0,0 +1,175 @@
/*
* https://github.com/Tampermonkey/tampermonkey/issues/2215
*
* This script provides a workaround for a Chrome MV3 issue (https://github.com/w3c/webextensions/issues/694)
* where extensions can't set/delete headers that are preserved over redirects.
*
* By setting `redirect: 'manual'` and following redirects manually, this script ensures request redirects work
* as intended and requests to different URLs are made in parallel (again).
*
* Userscript authors should include this as a `@require` when they need to make parallel requests with GM_xmlhttpRequest,
* especially if requests might take a long time to complete.
*
* Including this script will modify the behavior of GM_xmlhttpRequest and GM.xmlHttpRequest in Tampermonkey only.
*
* Usage:
*
* Add this to the metadata block of your userscript:
*
* // @grant GM_xmlhttpRequest
* // @require https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js
*
**/
/* global GM_info, GM_xmlhttpRequest, GM */
const HAS_GM = typeof GM !== 'undefined';
const NEW_GM = ((scope, GM) => {
// Check if running in Tampermonkey and if version supports redirect control
if (typeof GM_info === 'undefined' || GM_info.scriptHandler !== "Tampermonkey" || compareVersions(GM_info.version, "5.3.2") < 0) return;
// Backup original functions
const GM_xmlhttpRequestOrig = GM_xmlhttpRequest;
const GM_xmlHttpRequestOrig = GM.xmlHttpRequest;
function compareVersions(v1, v2) {
const parts1 = v1.split('.').map(Number);
const parts2 = v2.split('.').map(Number);
const length = Math.max(parts1.length, parts2.length);
for (let i = 0; i < length; i++) {
const num1 = parts1[i] || 0;
const num2 = parts2[i] || 0;
if (num1 > num2) return 1;
if (num1 < num2) return -1;
}
return 0;
}
// Wrapper for GM_xmlhttpRequest
function GM_xmlhttpRequestWrapper(odetails) {
// If redirect is manually set, simply pass odetails to the original function
if (odetails.redirect !== undefined) {
return GM_xmlhttpRequestOrig(odetails);
}
// Warn if onprogress is used with settings incompatible with fetch mode used in background
if (odetails.onprogress || odetails.fetch === false) {
console.warn("Fetch mode does not support onprogress in the background.");
}
const {
onload,
onloadend,
onerror,
onabort,
ontimeout,
...details
} = odetails;
// Set redirect to manual and handle redirects
const handleRedirects = (initialDetails) => {
const request = GM_xmlhttpRequestOrig({
...initialDetails,
redirect: 'manual',
onload: function (response) {
if (response.status >= 300 && response.status < 400) {
const m = response.responseHeaders.match(/Location:\s*(\S+)/i);
// Follow redirect manually
const redirectUrl = m && m[1];
if (redirectUrl) {
const absoluteUrl = new URL(redirectUrl, initialDetails.url).href;
handleRedirects({...initialDetails, url: absoluteUrl});
return;
}
}
if (onload) onload.call(this, response);
if (onloadend) onloadend.call(this, response);
},
onerror: function (response) {
if (onerror) onerror.call(this, response);
if (onloadend) onloadend.call(this, response);
},
onabort: function (response) {
if (onabort) onabort.call(this, response);
if (onloadend) onloadend.call(this, response);
},
ontimeout: function (response) {
if (ontimeout) ontimeout.call(this, response);
if (onloadend) onloadend.call(this, response);
}
});
return request;
};
return handleRedirects(details);
}
// Wrapper for GM.xmlHttpRequest
function GM_xmlHttpRequestWrapper(odetails) {
let abort;
const p = new Promise((resolve, reject) => {
const {onload, ontimeout, onerror, ...send} = odetails;
send.onerror = function (r) {
if (onerror) {
resolve(r);
onerror.call(this, r);
} else {
reject(r);
}
};
send.ontimeout = function (r) {
if (ontimeout) {
// See comment above
resolve(r);
ontimeout.call(this, r);
} else {
reject(r);
}
};
send.onload = function (r) {
resolve(r);
if (onload) onload.call(this, r);
};
const a = GM_xmlhttpRequestWrapper(send).abort;
if (abort === true) {
a();
} else {
abort = a;
}
});
p.abort = () => {
if (typeof abort === 'function') {
abort();
} else {
abort = true;
}
};
return p;
}
// Export wrappers
GM_xmlhttpRequest = GM_xmlhttpRequestWrapper;
scope.GM_xmlhttpRequestOrig = GM_xmlhttpRequestOrig;
const gopd = Object.getOwnPropertyDescriptor(GM, 'xmlHttpRequest');
if (gopd && gopd.configurable === false) {
return {
__proto__: GM,
xmlHttpRequest: GM_xmlHttpRequestWrapper,
xmlHttpRequestOrig: GM_xmlHttpRequestOrig
};
} else {
GM.xmlHttpRequest = GM_xmlHttpRequestWrapper;
GM.xmlHttpRequestOrig = GM_xmlHttpRequestOrig;
}
})(globalThis, HAS_GM ? GM : {});
if (HAS_GM && NEW_GM) GM = NEW_GM;
+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};
+37 -22
View File
@@ -1,10 +1,14 @@
import './core/globals.js';
import './lib/gm-xhr-parallel.js';
import { __defProp, __typeError, __publicField, __privateMethod, currentHref, isJavDb, isJavBus, isSearchPage, Status_RUNNING, Status_SUCCESS, Status_FAIL, Status_LOADING, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH, NO, YES, qualityOptions, detailPageCss$1, javBusStyle, detailPageCss, javdbStyle, mainCss, insertStyle } from './core/constants.js';
import { StorageManager } from './core/storage.js';
import { Utils } from './core/utils.js';
import { GmHttp } from './core/gmHttp.js';
import { PluginManager } from './core/plugin-manager.js';
import { BasePlugin } from './core/base-plugin.js';
import {TabulatorFull as Tabulator} from 'tabulator-tables';
window.Tabulator = Tabulator;
import { javDbApi, buildSignature, apiUrl } from './api/javdb.js';
window.javDbApi = javDbApi;
@@ -16,6 +20,8 @@ import { DetailPagePlugin } from './plugins/detail-page.js';
import { PreviewVideoPlugin } from './plugins/preview-video.js';
import { JavTrailersPlugin } from './plugins/jav-trailers.js';
import { SubTitleCatPlugin } from './plugins/subtitle-cat.js';
import {JavPackSubtitle} from './plugins/subtitles.js';
import {Req115} from './plugins/req115.js';
import { Fc2Plugin } from './plugins/fc2.js';
import { HighlightMagnetPlugin } from './plugins/highlight-magnet.js';
import { FoldCategoryPlugin } from './plugins/fold-category.js';
@@ -26,6 +32,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 {JavopenPreviewVideoPlugin} from './plugins/javopen-preview-video.js';
import { ImageRecognitionPlugin } from './plugins/image-recognition.js';
import { BusNavBarPlugin } from './plugins/bus-nav-bar.js';
import { RelatedPlugin } from './plugins/related.js';
@@ -48,6 +55,7 @@ import { DetailPageButtonPlugin } from './plugins/detail-page-button.js';
import { BlacklistPlugin } from './plugins/blacklist.js';
import { ListPagePlugin } from './plugins/list-page.js';
import { AutoPagePlugin } from './plugins/auto-page.js';
import {ArticlePagePlugin} from './plugins/article-page.js';
const _unsafeWindow = (() => typeof unsafeWindow !== 'undefined' ? unsafeWindow : window)();
@@ -816,29 +824,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");
@@ -846,7 +856,7 @@ utils.importResource("https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toasti
utils.importResource("https://cdn.jsdelivr.net/npm/viewerjs@1.11.1/dist/viewer.min.css");
utils.importResource("https://cdn.jsdelivr.net/npm/tabulator-tables@6.3.1/dist/css/tabulator_semanticui.min.css");
utils.importResource("https://cdn.jsdelivr.net/npm/tabulator-tables@6.5.2/dist/css/tabulator_semanticui.min.css");
const pluginManager = function() {
const pluginManager2 = new PluginManager;
@@ -855,6 +865,7 @@ const pluginManager = function() {
if (isJavDb) {
pluginManager2.register(ListPagePlugin);
pluginManager2.register(AutoPagePlugin);
pluginManager2.register(ArticlePagePlugin);
pluginManager2.register(Fc2Plugin);
pluginManager2.register(FoldCategoryPlugin);
pluginManager2.register(ListPageButtonPlugin);
@@ -873,6 +884,7 @@ const pluginManager = function() {
pluginManager2.register(DetailPageButtonPlugin);
pluginManager2.register(HighlightMagnetPlugin);
pluginManager2.register(PreviewVideoPlugin);
pluginManager2.register(JavopenPreviewVideoPlugin);
pluginManager2.register(FilterTitleKeywordPlugin);
pluginManager2.register(ActressInfoPlugin);
pluginManager2.register(OtherSitePlugin);
@@ -903,12 +915,15 @@ const pluginManager = function() {
pluginManager2.register(FilterTitleKeywordPlugin);
pluginManager2.register(HighlightMagnetPlugin);
pluginManager2.register(BusPreviewVideoPlugin);
pluginManager2.register(JavopenPreviewVideoPlugin);
pluginManager2.register(MagnetHubPlugin);
pluginManager2.register(ScreenShotPlugin);
pluginManager2.register(OtherSitePlugin);
pluginManager2.register(WangPan115TaskPlugin);
pluginManager2.register(TranslatePlugin);
pluginManager2.register(BlacklistPlugin);
pluginManager2.register(FavoriteActressesPlugin);
pluginManager2.register(FoldCategoryPlugin);
pluginManager2.register(TaskPlugin);
}
hostname.includes("javtrailers") && pluginManager2.register(JavTrailersPlugin);
+178
View File
@@ -0,0 +1,178 @@
import {BasePlugin} from '../core/base-plugin.js';
import {insertStyle} from '../core/constants.js';
class ArticlePagePlugin extends BasePlugin {
getName() {
return "ArticlePagePlugin";
}
handle() {
const href = window.location.href;
if (!href.includes("/articles")) {
return;
}
this.optimizeArticleLayout();
this.bindClick();
}
bindClick() {
// 使用 MutationObserver 监听 DOM 变化,尽早绑定事件
const observer = new MutationObserver((mutations) => {
for (const mutation of mutations) {
if (mutation.addedNodes.length > 0) {
this.attachClickHandler();
break;
}
}
});
// 立即尝试绑定一次
this.attachClickHandler();
// 监听整个 document 的 DOM 变化
observer.observe(document.body, {
childList: true,
subtree: true
});
}
attachClickHandler() {
const $articleList = $('.article-list');
if ($articleList.length === 0) return;
// 检查是否已经绑定过
if ($articleList.data('click-bound')) return;
$articleList.data('click-bound', true);
// 方法1:移除所有 a 标签的 href 属性,从根本上阻止跳转
$articleList.find('a').each(function () {
const $a = $(this);
const href = $a.attr('href');
if (href) {
$a.attr('data-href', href).removeAttr('href');
$a.css('cursor', 'pointer');
}
});
// 方法2:使用事件捕获阶段绑定(优先级最高)
document.querySelector('.article-list').addEventListener('click', (e) => {
// 简化选择器,closest 不支持 > div 这样的子选择器
let target = e.target.closest('.article-item, a, [data-href]');
// 如果没找到,尝试向上找 .article-list 的直接子元素
if (!target) {
let el = e.target;
const articleList = document.querySelector('.article-list');
while (el && el !== articleList) {
if (el.parentElement === articleList) {
target = el;
break;
}
el = el.parentElement;
}
}
if (!target) return;
e.preventDefault();
e.stopPropagation();
e.stopImmediatePropagation();
const $target = $(target);
const $link = $target.find('a[data-href]').first().length > 0
? $target.find('a[data-href]').first()
: $target.closest('a[data-href]');
const articleUrl = $link.attr('data-href') || $target.attr('data-href');
if (!articleUrl) return;
layer.open({
type: 2,
title: $target.text().trim().substring(0, 50) || '文章详情',
content: articleUrl,
area: ['80%', '85%'],
shadeClose: true,
maxmin: true,
scrollbar: false
});
return false;
}, true); // true = 捕获阶段,优先级最高
}
optimizeArticleLayout() {
const css = `
/* 文章列表容器改为 grid 布局,每行3个 */
.article-list {
display: grid !important;
grid-template-columns: repeat(3, 1fr) !important;
gap: 20px !important;
}
/* 每个文章卡片改为纵向布局 */
.article-list > div {
display: flex !important;
flex-direction: column !important;
overflow: hidden !important;
border-radius: 8px !important;
box-shadow: 0 2px 8px rgba(0,0,0,0.1) !important;
transition: transform 0.2s, box-shadow 0.2s !important;
background: #fff !important;
}
.article-list > div:hover {
transform: translateY(-4px) !important;
box-shadow: 0 4px 16px rgba(0,0,0,0.15) !important;
}
/* 封面图片 */
.article-list > div img {
width: 100% !important;
height: auto !important;
aspect-ratio: 3/4 !important;
object-fit: cover !important;
border-radius: 8px 8px 0 0 !important;
display: block !important;
}
/* 内容区域 */
.article-list > div > div:not(:first-child) {
padding: 12px !important;
display: flex !important;
flex-direction: column !important;
gap: 8px !important;
}
/* 标题完全显示,不截断 */
.article-list > div > a,
.article-list > div > h3,
.article-list > div > p {
font-size: 14px !important;
line-height: 1.5 !important;
display: block !important;
overflow: visible !important;
text-overflow: clip !important;
-webkit-line-clamp: unset !important;
-webkit-box-orient: unset !important;
min-height: unset !important;
color: #333 !important;
word-break: break-word !important;
}
/* 底部信息 */
.article-list > div > div:last-child {
margin-top: auto !important;
font-size: 12px !important;
color: #999 !important;
display: flex !important;
justify-content: space-between !important;
align-items: center !important;
}
/* 标签 */
.article-list > div span[class*="tag"],
.article-list > div span[class*="label"] {
background: #f0f0f0 !important;
padding: 2px 8px !important;
border-radius: 4px !important;
font-size: 11px !important;
}
`;
insertStyle(css);
}
}
export {ArticlePagePlugin};
+8 -7
View File
@@ -55,9 +55,6 @@ class AutoPagePlugin extends BasePlugin {
const nextLink = document.querySelector(selector.nextPageSelector);
this.nextUrl = null == nextLink ? void 0 : nextLink.href;
this.hasMore = !!this.nextUrl;
setTimeout((() => {
this.checkLoad();
}), 1e3);
this.hasMore || this.setState("waterfall-no-more", "已经到底了");
}
async loadNextPage() {
@@ -128,7 +125,9 @@ class AutoPagePlugin extends BasePlugin {
clog.error("加载失败:", e);
this.setState("waterfall-error", "加载失败,点击重试");
} finally {
this.isLoading = !1;
setTimeout((() => {
this.isLoading = !1;
}), 600);
}
}
checkScrollPosition() {
@@ -145,13 +144,15 @@ class AutoPagePlugin extends BasePlugin {
}
}
checkLoad() {
if (!this.loader) return;
this.loader.getBoundingClientRect().top < window.innerHeight + this.preloadDistance && this.loadNextPage().then();
if (!this.loader || this.isLoading || !this.hasMore) return;
if (window.scrollY < 100) return;
const triggerDistance = 200;
this.loader.getBoundingClientRect().top < window.innerHeight + triggerDistance && this.loadNextPage().then();
}
async shouldDisablePaging() {
if (!window.isListPage) return !0;
await storageManager.getSetting("autoPage", YES);
return [ "search?q", "handlePlayback=1", "handleTop=1", "/want_watch_videos", "/watched_videos", "/advanced_search?type=100" ].some((path => currentHref.includes(path)));
return ["search?q", "/rankings/playback", "/rankings/top", "/want_watch_videos", "/watched_videos", "/advanced_search?type=100"].some((path => currentHref.includes(path)));
}
updatePageUrl_old(href) {
window.history.pushState({}, "", href);
+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
+294 -14
View File
@@ -1,6 +1,19 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH, NO, YES } from '../core/constants.js';
import {
__publicField,
isJavBus,
isJavDb,
isEligibleDmmCoverCode,
Status_FILTER,
Status_FAVORITE,
Status_HAS_DOWN,
Status_HAS_WATCH,
NO,
YES
} from '../core/constants.js';
import { HotkeyManager } from './preview-video.js';
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
import {getCover} from '../api/dmmrsc.js';
class DetailPageButtonPlugin extends BasePlugin {
getName() {
@@ -22,7 +35,8 @@ class DetailPageButtonPlugin extends BasePlugin {
window.isDetailPage && this.createMenuBtn();
}
async createMenuBtn() {
const pageInfo = this.getPageInfo(), carNum2 = pageInfo.carNum, buttonsHtml = '\n <div style="margin: 10px auto; display: flex; justify-content: space-between; align-items: center; flex-wrap:wrap;gap: 20px;">\n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="filterBtn" class="menu-btn" style="width: 120px; background-color:#de3333; color: white; text-align: center; padding: 8px 0;">\n <span>🚫 屏蔽</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="width: 120px; background-color:#25b1dc; color: white; text-align: center; padding: 8px 0;">\n <span>⭐ 收藏</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="width: 120px; background-color:#7bc73b; color: white; text-align: center; padding: 8px 0;">\n <span>📥️ 已下载</span>\n </a>\n <a id="hasWatchBtn" class="menu-btn" style="width: 120px; background-color:#d7a80c; color: white; text-align: center; padding: 8px 0;">\n <span>🔍 已观看</span>\n </a>\n </div>\n \n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="enable-magnets-filter" class="menu-btn" style="width: 140px; background-color: #c2bd4c; color: white; text-align: center; padding: 8px 0;">\n <span id="magnets-span">关闭磁力过滤</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="search-subtitle-btn" class="menu-btn" style="width: 160px; background: linear-gradient(to bottom, #8d5656, rgb(196,159,91)); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (SubTitleCat)</span>\n </a>\n </div>\n </div>\n ';
const pageInfo = this.getPageInfo(), carNum2 = pageInfo.carNum,
buttonsHtml = '\n <div style="margin: 10px auto; display: flex; justify-content: space-between; align-items: center; flex-wrap:wrap;gap: 20px;">\n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="filterBtn" class="menu-btn" style="width: 120px; background-color:#de3333; color: white; text-align: center; padding: 8px 0;">\n <span>🚫 屏蔽</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="width: 120px; background-color:#25b1dc; color: white; text-align: center; padding: 8px 0;">\n <span>⭐ 收藏</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="width: 120px; background-color:#7bc73b; color: white; text-align: center; padding: 8px 0;">\n <span>📥️ 已下载</span>\n </a>\n <a id="hasWatchBtn" class="menu-btn" style="width: 120px; background-color:#d7a80c; color: white; text-align: center; padding: 8px 0;">\n <span>🔍 已观看</span>\n </a>\n </div>\n \n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="downloadCoverBtn" class="menu-btn" style="width: 140px; background-color: #9b59b6; color: white; text-align: center; padding: 8px 0;">\n <span>下载高清封面</span>\n </a>\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()));
@@ -42,25 +56,288 @@ 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);
}
}));
$("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
$("#xunLeiSubtitleBtn").on("click", (() => this.searchXunLeiSubtitle(carNum2)));
$("#subtitleSearchBtn").on("click", (() => {
window.JavPackSubtitle.openSearchModal({details: {code: carNum2}});
}));
$("#downloadCoverBtn").on("click", (async () => {
if (!isEligibleDmmCoverCode(carNum2)) {
show.error("当前番号不支持下载高清封面");
return;
}
const $btn = $("#downloadCoverBtn");
$btn.addClass("is-loading").prop("disabled", true);
try {
const coverUrl = await getCover(carNum2);
if (!coverUrl) {
show.error("未找到高清封面");
return;
}
const fileName = `${carNum2}-cover.jpg`;
GM_xmlhttpRequest({
method: 'GET',
url: coverUrl,
responseType: 'blob',
onload: (response) => {
if (response.status === 200) {
const blobUrl = URL.createObjectURL(response.response);
const a = document.createElement('a');
a.href = blobUrl;
a.download = fileName;
document.body.appendChild(a);
a.click();
setTimeout(() => {
document.body.removeChild(a);
URL.revokeObjectURL(blobUrl);
}, 100);
show.ok("封面下载已开始");
} else {
show.error(`封面下载失败: ${response.status}`);
}
},
onerror: () => show.error("封面下载网络错误"),
ontimeout: () => show.error("封面下载超时"),
});
} catch (e) {
show.error("获取封面信息失败");
} finally {
$btn.removeClass("is-loading").prop("disabled", false);
}
}));
window.JavPackSubtitle.preload115Matches(carNum2);
// $("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
// $("#xunLeiSubtitleBtn").on("click", (() => this.searchXunLeiSubtitle(carNum2)));
this.showStatus(carNum2).then();
}
async 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)}`);
@@ -124,6 +401,8 @@ class DetailPageButtonPlugin extends BasePlugin {
window.refresh();
utils.closePage();
}
/*
searchXunLeiSubtitle(carNum2) {
let loadObj = loading();
gmHttp.get(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?gcid=&cid=&name=${carNum2}`).then((res => {
@@ -205,6 +484,7 @@ class DetailPageButtonPlugin extends BasePlugin {
loadObj.close();
}));
}
*/
async filterOne(event, noAlert) {
event && event.preventDefault();
let pageInfo = this.getPageInfo();
@@ -333,4 +613,4 @@ class DetailPageButtonPlugin extends BasePlugin {
}
export { DetailPageButtonPlugin };
export { DetailPageButtonPlugin };
+7
View File
@@ -5,6 +5,13 @@ class FavoriteActressesPlugin extends BasePlugin {
getName() {
return "FavoriteActressesPlugin";
}
async initCss() {
const settingObj = await storageManager.getSetting();
const tagNumber = settingObj.highlightedTagNumber || 1;
const tagColor = settingObj.highlightedTagColor || "#ce2222";
return `\n <style>\n .highlighted {\n border: ${tagNumber}px solid ${tagColor} !important;\n }\n </style>\n `;
}
async handle() {
this.bindEvent();
await this.highlightActress();
+9 -5
View File
@@ -171,7 +171,7 @@ class Fc2By123AvPlugin extends BasePlugin {
async open123AvFc2Dialog(carNum2, href) {
let otherHtml = "";
await storageManager.getSetting("enableLoadOtherSite", YES) === YES && (otherHtml = '<div class="movie-panel-info fc2-movie-panel-info" style="margin-top:20px"><strong>第三方站点: </strong></div>');
let pageHtml = `\n <div class="movie-detail-container">\n \x3c!-- <div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>\n <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n ${otherHtml}\n \n <div style="margin: 10px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\n \n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links">\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!-- </div>--\x3e\n </div>\n `;
let pageHtml = `\n <div class="movie-detail-container">\n \x3c!-- <div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>\n <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n ${otherHtml}\n \n <div style="margin: 10px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\n \n <a id="subtitleSearchBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕搜索</span>\n </a>\n <!--\n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n -->\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links">\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!-- </div>--\x3e\n </div>\n `;
layer.open({
type: 1,
title: carNum2,
@@ -235,8 +235,12 @@ class Fc2By123AvPlugin extends BasePlugin {
window.refresh();
layer.closeAll();
}));
$("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
$("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2)));
$("#subtitleSearchBtn").on("click", (() => {
window.JavPackSubtitle.openSearchModal({details: {code: carNum2}});
}));
window.JavPackSubtitle.preload115Matches(carNum2);
// $("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
// $("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2)));
let tempCarNum = carNum2.replace("FC2-", "");
this.getBean("OtherSitePlugin").loadOtherSite(tempCarNum, carNum2).then();
}
@@ -265,7 +269,7 @@ class Fc2By123AvPlugin extends BasePlugin {
$(".screen-container").on("click", (event => {
event.stopPropagation();
event.preventDefault();
showImageViewer(event.currentTarget);
showPreviewOverlay(imgUrl, carNum2);
}));
}
}));
@@ -351,4 +355,4 @@ class Fc2By123AvPlugin extends BasePlugin {
}
export { Fc2By123AvPlugin };
export { Fc2By123AvPlugin };
+8 -4
View File
@@ -33,7 +33,7 @@ class Fc2Plugin extends BasePlugin {
layer.open({
type: 1,
title: carNum2,
content: '\n <div class="movie-detail-container">\n \x3c!--<div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>--\x3e\n \x3c!-- <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n <div class="movie-panel-info fc2-movie-panel-info" style="margin-top:20px"><strong>第三方资源: </strong></div>\n \n <div style="margin: 30px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\n \n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn fr-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links" style="margin: 0 0.75rem">\n <div class="search-loading">加载中...</div>\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!--</div>--\x3e\n </div>\n ',
content: '\n <div class="movie-detail-container">\n \x3c!--<div class="movie-poster-container">\n <iframe class="movie-trailer" frameborder="0" allowfullscreen scrolling="no"></iframe>\n </div>--\x3e\n \x3c!-- <div class="right-box">--\x3e\n <div class="movie-info-container">\n <div class="search-loading">加载中...</div>\n </div>\n \n <div class="movie-panel-info fc2-movie-panel-info" style="margin-top:20px"><strong>第三方资源: </strong></div>\n \n <div style="margin: 30px 0">\n <a id="filterBtn" class="menu-btn" style="background-color:#de3333"><span>🚫 屏蔽</span></a>\n <a id="favoriteBtn" class="menu-btn" style="background-color:#25b1dc"><span>⭐ 收藏</span></a>\n <a id="hasDownBtn" class="menu-btn" style="background-color:#7bc73b"><span>📥️ 已下载</span></a>\n <a id="hasWatchBtn" class="menu-btn" style="background-color:#d7a80c;"><span>🔍 已观看</span></a>\n \n <a id="subtitleSearchBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕搜索</span>\n </a>\n <!--\n <a id="search-subtitle-btn" class="menu-btn fr-btn" style="background:linear-gradient(to bottom, #8d5656, rgb(196,159,91))">\n <span>字幕 (SubTitleCat)</span>\n </a>\n <a id="xunLeiSubtitleBtn" class="menu-btn fr-btn" style="background:linear-gradient(to left, #375f7c, #2196F3)">\n <span>字幕 (迅雷)</span>\n </a>\n -->\n <a id="magnetSearchBtn" class="menu-btn fr-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n </div>\n <div class="message video-panel" style="margin-top:20px">\n <div id="magnets-content" class="magnet-links" style="margin: 0 0.75rem">\n <div class="search-loading">加载中...</div>\n </div>\n </div>\n <div id="reviews-content">\n </div>\n <div id="related-content">\n </div>\n <span id="data-actress" style="display: none"></span>\n \x3c!--</div>--\x3e\n </div>\n ',
area: utils.getResponsiveArea([ "70%", "90%" ]),
skin: "movie-detail-layer",
scrollbar: !1,
@@ -90,8 +90,12 @@ class Fc2Plugin extends BasePlugin {
window.refresh();
layer.closeAll();
}));
$("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
$("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2)));
$("#subtitleSearchBtn").on("click", (() => {
window.JavPackSubtitle.openSearchModal({details: {code: carNum2}});
}));
window.JavPackSubtitle.preload115Matches(carNum2);
// $("#search-subtitle-btn").on("click", (event => utils.openPage(`https://subtitlecat.com/index.php?search=${carNum2}`, carNum2, !1, event)));
// $("#xunLeiSubtitleBtn").on("click", (() => this.getBean("DetailPageButtonPlugin").searchXunLeiSubtitle(carNum2)));
$("#magnetSearchBtn").on("click", (() => {
let magnetHub = this.getBean("MagnetHubPlugin").createMagnetHub(carNum2);
layer.open({
@@ -147,7 +151,7 @@ class Fc2Plugin extends BasePlugin {
utils.loopDetector((() => $(".movie-gallery .image-list").length > 0), (async () => {
$(".movie-gallery .image-list").prepend(' <a class="tile-item screen-container" style="overflow:hidden;max-height: 150px;max-width:150px; text-align:center;"><div style="margin-top: 50px;color: #000;cursor: auto">正在加载缩略图</div></a> ');
const screenShotPlugin = this.getBean("ScreenShotPlugin"), imgUrl = await screenShotPlugin.getScreenshot(carNum2);
imgUrl && await screenShotPlugin.addImg("缩略图", imgUrl);
imgUrl && await screenShotPlugin.addImg("缩略图", imgUrl, carNum2);
}));
}
handleMagnets(movieId) {
+5 -2
View File
@@ -7,7 +7,10 @@ class FoldCategoryPlugin extends BasePlugin {
}
async initCss() {
const settingObj = await storageManager.getSetting();
return `\n <style>\n #tags a.tag, .tags a.tag {\n position:relative;\n }\n .highlight-btn {\n position: absolute;\n top: -10px;\n right: -10px;\n background-color: #4CAF50;\n color: white;\n border: none;\n border-radius: 50%;\n width: 24px;\n height: 24px;\n font-size: 14px;\n line-height: 24px;\n text-align: center;\n cursor: pointer;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n display: none;\n z-index: 999;\n }\n /* 当父元素被高亮时,按钮变为其他颜色 */\n .highlighted .highlight-btn {\n background-color: #FF5722;\n }\n /* 高亮状态下的标签样式 */\n .highlighted {\n /* 浅黄色 */\n border: ${settingObj.highlightedTagNumber || 1}px solid ${settingObj.highlightedTagColor || "#ce2222"};\n }\n </style>\n `;
return `\n <style>\n #tags a.tag, .tags a.tag {\n position:relative;\n }\n .highlight-btn {\n position: absolute;\n top: -10px;\n right: -10px;\n background-color: #4CAF50;\n color: white;\n border: none;\n border-radius: 50%;\n width: 24px;\n height: 24px;\n font-size: 14px;\n line-height: 24px;\n text-align: center;\n cursor: pointer;\n box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2);\n display: none;\n z-index: 999;\n }\n /* 当父元素被高亮时,按钮变为其他颜色 */\n .highlighted .highlight-btn {\n background-color: #FF5722;\n }\n /* 高亮状态下的标签样式 */\n .highlighted {
/* 浅黄色 */
border: ${settingObj.highlightedTagNumber || 1}px solid ${settingObj.highlightedTagColor || "#ce2222"} !important;
}\n </style>\n `;
}
async handle() {
if (window.isListPage && !currentHref.includes("advanced_search")) {
@@ -82,4 +85,4 @@ class FoldCategoryPlugin extends BasePlugin {
}
export { FoldCategoryPlugin };
export { FoldCategoryPlugin };
+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()));
}
}
+4 -2
View File
@@ -222,7 +222,9 @@ class HistoryPlugin extends BasePlugin {
ajaxURL: "queryRealm",
dataLoader: !1,
ajaxRequestFunc: async (url, ajaxConfig, params) => {
const page = params.page, size = params.size, sort = params.sort;
const page = (null == params ? void 0 : params.page) || 1,
size = (null == params ? void 0 : params.size) || 50,
sort = (null == params ? void 0 : params.sort) || [];
return await this.getDataList(page, size, sort);
},
dataReceiveParams: {
@@ -476,4 +478,4 @@ class HistoryPlugin extends BasePlugin {
}
export { HistoryPlugin };
export { HistoryPlugin };
+4 -3
View File
@@ -14,7 +14,7 @@ class HitShowPlugin extends BasePlugin {
$('a[href*="rankings/playback"]').on("click", (event => {
event.preventDefault();
event.stopPropagation();
window.location.href = "/advanced_search?handlePlayback=1&period=daily";
window.location.href = "/rankings/playback";
}));
this.handlePlayback().then();
}
@@ -29,7 +29,7 @@ class HitShowPlugin extends BasePlugin {
this.$contentBox.append('<div class="movie-list h cols-4 vcols-8" style="margin-top: 10px"></div>');
}
async handlePlayback() {
if (!window.location.href.includes("handlePlayback=1")) return;
if (!window.location.href.includes("rankings/playback")) return;
let period = new URLSearchParams(window.location.search).get("period");
this.toolBar(period);
this.hookPage();
@@ -40,6 +40,7 @@ class HitShowPlugin extends BasePlugin {
const movies = await javDbApi.playback(period);
let moviesHtml = javDbApi.markDataListHtml(movies);
$movieBox.html(moviesHtml);
this.pluginManager.getBean("ListPagePlugin").doFilter();
} catch (e) {
clog.error("发生错误:", e);
} finally {
@@ -47,7 +48,7 @@ class HitShowPlugin extends BasePlugin {
}
}
toolBar(period) {
let conditionHtml = `\n <div class="button-group" style="margin-top:18px">\n <div class="buttons has-addons" id="conditionBox">\n <a style="padding:18px 18px !important;" class="button is-small ${"daily" === period ? "is-info" : ""}" href="/advanced_search?handlePlayback=1&period=daily">日榜</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"weekly" === period ? "is-info" : ""}" href="/advanced_search?handlePlayback=1&period=weekly">周榜</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"monthly" === period ? "is-info" : ""}" href="/advanced_search?handlePlayback=1&period=monthly">月榜</a>\n </div>\n </div>\n `;
let conditionHtml = `\n <div class="button-group" style="margin-top:18px">\n <div class="buttons has-addons" id="conditionBox">\n <a style="padding:18px 18px !important;" class="button is-small ${"daily" === period ? "is-info" : ""}" href="/rankings/playback?period=daily">日榜</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"weekly" === period ? "is-info" : ""}" href="/rankings/playback?period=weekly">周榜</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"monthly" === period ? "is-info" : ""}" href="/rankings/playback?period=monthly">月榜</a>\n </div>\n </div>\n `;
this.$contentBox.append(conditionHtml);
}
getStarRating(score) {
+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();
+232
View File
@@ -0,0 +1,232 @@
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 {getTrailer} from '../api/dmmrsc.js';
class JavopenPreviewVideoPlugin extends BasePlugin {
getName() {
return "JavopenPreviewVideoPlugin";
}
_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);
});
} else {
loadHlsLibrary().then((HlsClass) => {
if (videoEl && videoEl.isConnected && HlsClass) {
attachHlsToVideo(videoEl, src).catch((err) => {
clog.error('视频预览 HLS 延迟加载失败:', err);
});
}
});
}
} 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-javopen-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-javopen-video-modal.is-open { opacity: 1; visibility: visible; }
.jhs-javopen-video-modal-inner {
display: flex; flex-direction: column; align-items: center;
gap: 12px; max-width: 90vw; max-height: 90vh;
}
.jhs-javopen-video-wrapper {
width: 80vw; max-height: 80vh; aspect-ratio: 16/9;
background: #000; position: relative;
}
.jhs-javopen-video-wrapper video {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}
.jhs-javopen-quality-bar {
display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;
}
.jhs-javopen-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-javopen-quality-btn:hover { background: rgba(255,255,255,0.4); }
.jhs-javopen-quality-btn.active {
background: #1890ff; border-color: #096dd9; font-weight: bold;
}
.jhs-javopen-loading {
color: #fff; font-size: 16px; text-align: center;
}
`;
}
async handle() {
if (!window.isDetailPage) return;
const carNum = this.getPageInfo().carNum;
if (!carNum) return;
this._addJavopenButton(carNum);
}
_addJavopenButton(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._openJavopenPlayer(carNum);
} finally {
$btn.css("pointer-events", "").find("span").text("视频预览");
}
});
$target.after($btn);
}
async _openJavopenPlayer(carNum) {
if ($("#jhs-javopen-video-modal").length === 0) {
$("body").append(`
<div id="jhs-javopen-video-modal" class="jhs-javopen-video-modal">
<div class="jhs-javopen-video-modal-inner">
<div class="jhs-javopen-video-wrapper">
<video id="jhs-javopen-video" controls playsinline></video>
</div>
<div class="jhs-javopen-quality-bar"></div>
</div>
</div>
`);
const $modal = $("#jhs-javopen-video-modal");
$modal.on("click", (e) => {
if (e.target.id === "jhs-javopen-video-modal") {
$modal.removeClass("is-open");
const videoEl = document.getElementById("jhs-javopen-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-javopen-video");
if (videoEl) {
destroyHls(videoEl);
videoEl.pause();
}
}
});
}
const $modal = $("#jhs-javopen-video-modal");
const $wrapper = $modal.find(".jhs-javopen-video-wrapper");
const $qualityBar = $modal.find(".jhs-javopen-quality-bar");
$wrapper.html('<div class="jhs-javopen-loading">视频预览加载中...</div>');
$qualityBar.empty();
$modal.addClass("is-open");
let videoMap = null;
try {
videoMap = await getTrailer(carNum);
} catch (err) {
$wrapper.html(`<div class="jhs-javopen-loading">视频预览请求失败: ${err.message}</div>`);
return;
}
if (!videoMap || Object.keys(videoMap).length === 0) {
$wrapper.html('<div class="jhs-javopen-loading">未找到可用的视频源</div>');
return;
}
const qualityList = Object.keys(videoMap);
const defaultQuality = this._selectDefaultQuality(qualityList, '720p');
const defaultUrl = videoMap[defaultQuality];
$wrapper.html(`<video id="jhs-javopen-video" controls playsinline>
<source src="${defaultUrl}" />
</video>`);
const videoEl = document.getElementById("jhs-javopen-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-javopen-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-javopen-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-javopen-quality-btn").removeClass("active");
$btn.addClass("active");
});
videoEl.play().catch((e) => console.warn("视频预览播放失败:", e));
}
}
export {JavopenPreviewVideoPlugin};
+78 -1
View File
@@ -10,6 +10,7 @@ class ListPageButtonPlugin extends BasePlugin {
await this.createMenuBtn();
this.bindEvent();
await storageManager.getSetting("autoPage") === YES ? $("#sort-toggle-btn").hide() : this.sortItems().then();
this.initOneTimeSortBtn();
}
async createMenuBtn() {
const showWaitCheckBtn = await storageManager.getSetting("showWaitCheckBtn", YES), showWaitDownBtn = await storageManager.getSetting("showWaitDownBtn", YES);
@@ -38,7 +39,7 @@ class ListPageButtonPlugin extends BasePlugin {
const isFc2Page2 = currentHref.includes("advanced_search");
isFc2Page2 ? $el = $("h2.section-title") : otherCss = "flex-grow:1;";
const jhs_sortMethod = localStorage.getItem("jhs_sortMethod"), sortText = "当前排序方式: " + ("rateCount" === jhs_sortMethod ? "评价人数" : "date" === jhs_sortMethod ? "时间" : "默认");
$el.append(`\n <div style="display: flex;align-items: center; ${otherCss} ">\n <a id="waitCheckBtn" class="menu-btn main-tab-btn" data-tip="打开未鉴定的列表项, 并自动播放视频" style="background-color:#56c938 !important; ${showWaitCheckBtn === NO ? "display: none" : ""}"><span>打开待鉴定</span></a>\n <a id="waitDownBtn" class="menu-btn main-tab-btn" style="background-color:#2caac0 !important; ${showWaitDownBtn === NO ? "display: none" : ""}"><span>打开已收藏</span></a>\n ${isStarPage ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${addBlacklistBtnColor} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${addBlacklistBtnText}</span></a>\n <a id="filterAllVideo" class="menu-btn main-tab-btn" style="background-color:#e8ab39 !important;margin-right: 30px!important;" data-tip="一键屏蔽已选分类的视频列表至鉴定记录中"><span>一键屏蔽所有作品</span></a>\n ` : ""}\n ${currentHref.includes("/tags") ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${addBlacklistBtnColor} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${addBlacklistBtnText}</span></a>\n ` : ""}\n </div>\n <div style="display: flex;align-items: center;">\n <a id="newVideoBtn" class="menu-btn main-tab-btn" style="background-color:#2c6cc0 !important;"><span>新作品检测 (<span id="newVideoCount">0</span>)</span></a>\n <a id="blacklistBtn" class="menu-btn main-tab-btn" style="background-color:#34393f !important;"><span>演员黑名单</span></a>\n ${isSearchPage || isFc2Page2 ? "" : `<a id="sort-toggle-btn" class="menu-btn main-tab-btn" style="background-color:#8783ab !important;"> ${sortText} </a>`}\n </div>\n `);
$el.append(`\n <div style="display: flex;align-items: center; ${otherCss} ">\n <a id="waitCheckBtn" class="menu-btn main-tab-btn" data-tip="打开未鉴定的列表项, 并自动播放视频" style="background-color:#56c938 !important; ${showWaitCheckBtn === NO ? "display: none" : ""}"><span>打开待鉴定</span></a>\n <a id="waitDownBtn" class="menu-btn main-tab-btn" style="background-color:#2caac0 !important; ${showWaitDownBtn === NO ? "display: none" : ""}"><span>打开已收藏</span></a>\n ${isStarPage ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${addBlacklistBtnColor} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${addBlacklistBtnText}</span></a>\n <a id="filterAllVideo" class="menu-btn main-tab-btn" style="background-color:#e8ab39 !important;margin-right: 30px!important;" data-tip="一键屏蔽已选分类的视频列表至鉴定记录中"><span>一键屏蔽所有作品</span></a>\n ` : ""}\n ${currentHref.includes("/tags") ? `\n <a id="addBlacklistBtn" class="menu-btn main-tab-btn" style="background-color:${addBlacklistBtnColor} !important;" data-tip="将演员加入黑名单, 后续有作品更新也会纳入屏蔽中"><span>${addBlacklistBtnText}</span></a>\n ` : ""}\n </div>\n <div style="display: flex;align-items: center;">\n <a id="one-time-sort-btn" class="menu-btn main-tab-btn" style="background-color:#8783ab !important;">一次性排序: 默认</a>\n <a id="newVideoBtn" class="menu-btn main-tab-btn" style="background-color:#2c6cc0 !important;"><span>新作品检测 (<span id="newVideoCount">0</span>)</span></a>\n <a id="blacklistBtn" class="menu-btn main-tab-btn" style="background-color:#34393f !important;"><span>演员黑名单</span></a>\n ${isSearchPage || isFc2Page2 ? "" : `<a id="sort-toggle-btn" class="menu-btn main-tab-btn" style="background-color:#8783ab !important;"> ${sortText} </a>`}\n </div>\n `);
}
if (isJavBus) {
const isStarPage = currentHref.includes("/star/");
@@ -90,6 +91,22 @@ class ListPageButtonPlugin extends BasePlugin {
localStorage.setItem("jhs_sortMethod", newMethod);
this.sortItems().then();
}));
$("#one-time-sort-btn").on("click", (event => {
const currentMethod = localStorage.getItem("jhs_oneTimeSortMethod") || "default";
const sortCycle = ["default", "rateCount", "totalScore", "date"];
let nextIndex = sortCycle.indexOf(currentMethod) + 1;
if (nextIndex >= sortCycle.length || nextIndex < 0) nextIndex = 0;
const newMethod = sortCycle[nextIndex];
const methodText = {
default: "默认",
rateCount: "评价人数",
totalScore: "总分",
date: "时间"
}[newMethod];
$("#one-time-sort-btn").text(`一次性排序: ${methodText}`);
localStorage.setItem("jhs_oneTimeSortMethod", newMethod);
this.oneTimeSortItems().then();
}));
const blacklistPlugin = this.getBean("BlacklistPlugin");
$("#addBlacklistBtn").on("click", (async event => {
await blacklistPlugin.addBlacklist(event);
@@ -174,6 +191,66 @@ class ListPageButtonPlugin extends BasePlugin {
$container.empty().append(items);
}
}
async initOneTimeSortBtn() {
const autoPage = await storageManager.getSetting("autoPage");
if (autoPage !== YES) {
$("#one-time-sort-btn").hide();
return;
}
const oneTimeMethod = localStorage.getItem("jhs_oneTimeSortMethod") || "default";
const methodText = {
default: "默认",
rateCount: "评价人数",
totalScore: "总分",
date: "时间"
}[oneTimeMethod];
$("#one-time-sort-btn").text(`一次性排序: ${methodText}`).show();
}
async oneTimeSortItems() {
if (currentHref.includes("handle") || currentHref.includes("advanced_search")) return;
if (isSearchPage) return;
const method = localStorage.getItem("jhs_oneTimeSortMethod");
if (!method) return;
$(".movie-list .item").each((function (index) {
$(this).attr("data-original-index") || $(this).attr("data-original-index", index);
}));
const $container = $(".movie-list"), $items = $(".item", $container);
if ("default" === method) {
$items.sort((function (a, b) {
return $(a).data("original-index") - $(b).data("original-index");
})).appendTo($container);
} else {
const items = $items.get();
items.sort((function (a, b) {
if ("rateCount" === method) {
const getScore = el => {
const match = $(el).find(".score .value").text().match(/由(\d+)人/);
return match ? parseFloat(match[1]) : 0;
};
return getScore(b) - getScore(a);
} else if ("totalScore" === method) {
const getTotalScore = el => {
const text = $(el).find(".score .value").text();
const scoreMatch = text.match(/([\d.]+)分/);
const countMatch = text.match(/由(\d+)人/);
const score = scoreMatch ? parseFloat(scoreMatch[1]) : 0;
const count = countMatch ? parseFloat(countMatch[1]) : 0;
return score * count;
};
return getTotalScore(b) - getTotalScore(a);
} else {
const getDate = el => {
const dateStr = $(el).find(".meta").text().trim();
return new Date(dateStr);
};
return getDate(b) - getDate(a);
}
}));
$container.empty().append(items);
}
}
async openWaitCheck(event) {
const maxCount = await storageManager.getSetting("waitCheckCount", 5);
let count = 0;
+18 -3
View File
@@ -56,9 +56,9 @@ class ListPagePlugin extends BasePlugin {
this.replaceHdImg();
this.addJumpPageControl();
this.fixBusTitleBox();
this.getBean("CoverButtonPlugin").addSvgBtn().then();
await this.doFilter();
await this.getBean("ListPageButtonPlugin").sortItems();
this.getBean("CoverButtonPlugin").addSvgBtn().then();
$(this.getSelector().itemSelector + " a").attr("target", "_blank");
this.getBean("AutoPagePlugin").checkLoad();
} finally {
@@ -151,7 +151,20 @@ class ListPagePlugin extends BasePlugin {
await Promise.all(movieList.map((async ele => {
let $box2 = $(ele);
if (isJavBus && $box2.find(".avatar-box").length > 0) return;
const {carNum: carNum2, title: title} = this.getBoxCarInfo($box2), {filter: filter, favorite: favorite, hasDown: hasDown, hasWatch: hasWatch} = carNumSets, isFavorite = favorite.has(carNum2), isHasDown = hasDown.has(carNum2), isHasWatch = hasWatch.has(carNum2), isFiltered = filter.has(carNum2), isFilterActorMale = actorCarNumToNameMap.has(carNum2), isFilterActorFemale = actressCarNumToNameMap.has(carNum2), isFilterActor = isFilterActorMale || isFilterActorFemale, foundKeyword = filterKeywordList.find((kw => title.includes(kw) || carNum2.startsWith(kw))), isFilterKeyword = !!foundKeyword;
const boxInfo = this.getBoxCarInfo($box2);
if (!boxInfo) return;
const {carNum: carNum2, title: title} = boxInfo, {
filter: filter,
favorite: favorite,
hasDown: hasDown,
hasWatch: hasWatch
} = carNumSets, isFavorite = favorite.has(carNum2), isHasDown = hasDown.has(carNum2),
isHasWatch = hasWatch.has(carNum2), isFiltered = filter.has(carNum2),
isFilterActorMale = actorCarNumToNameMap.has(carNum2),
isFilterActorFemale = actressCarNumToNameMap.has(carNum2),
isFilterActor = isFilterActorMale || isFilterActorFemale,
foundKeyword = filterKeywordList.find((kw => title.includes(kw) || carNum2.startsWith(kw))),
isFilterKeyword = !!foundKeyword;
if (!isSearchPage) {
let shouldHide = settings.showFavoriteItem === NO && isFavorite || settings.showHasDownItem === NO && isHasDown || settings.showHasWatchItem === NO && isHasWatch || settings.showFilterItem === NO && isFiltered && !(isFavorite || isHasDown || isHasWatch) || settings.showFilterActorItem === NO && isFilterActor || settings.showFilterKeywordItem === NO && isFilterKeyword;
if ($box2.attr("data-movieShowType") !== settings.movieShowType) {
@@ -402,11 +415,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 +439,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");
+1066 -142
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",
+528
View File
@@ -0,0 +1,528 @@
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
class Req {
static DEFAULT_GET_RESPONSE_TYPE = "document";
static DEFAULT_POST_RESPONSE_TYPE = "json";
static DEFAULT_TIMEOUT = 30000;
static DEFAULT_METHOD = "GET";
static _isRawBody = (v) => v instanceof FormData || v instanceof Blob || v instanceof ArrayBuffer || v instanceof URLSearchParams;
static _isPlainObject = (v) => Object.prototype.toString.call(v) === "[object Object]";
static _serializeToFormData(formData, data, parentKey = "") {
Object.entries(data).forEach(([key, val]) => {
const fullKey = parentKey ? `${parentKey}[${key}]` : key;
if (val === undefined) return;
if (val !== null && typeof val === "object" && !(val instanceof Blob || val instanceof Date)) {
this._serializeToFormData(formData, val, fullKey);
} else {
formData.append(fullKey, val instanceof Date ? val.toISOString() : val);
}
});
}
static request(config) {
let options;
if (typeof config === "string") {
options = {url: config};
} else if (config !== null && typeof config === "object") {
options = {...config};
} else {
return Promise.reject(new TypeError("Request config must be a string or an object"));
}
if (!options.url) return Promise.reject(new Error("Request URL is required"));
options.method = (options.method || this.DEFAULT_METHOD).toUpperCase();
options.timeout ||= this.DEFAULT_TIMEOUT;
const {params, method, data, signal} = options;
try {
const urlInstance = new URL(options.url, location.origin);
if (params) {
const cleanParams = this._isPlainObject(params) ? Object.fromEntries(Object.entries(params).filter(([_, v]) => v != null)) : params;
new URLSearchParams(cleanParams).forEach((v, k) => urlInstance.searchParams.set(k, v));
}
options.url = urlInstance.toString();
} catch {
return Promise.reject(new Error(`Invalid Request URL: ${options.url}`));
}
if (method === "POST") {
options.responseType ??= this.DEFAULT_POST_RESPONSE_TYPE;
if (this._isPlainObject(data) && !this._isRawBody(data)) {
const formData = new FormData();
this._serializeToFormData(formData, data);
options.data = formData;
}
} else if (method === "GET") {
options.responseType ??= options.url.split(/[?#]/)[0].endsWith(".json") ? "json" : this.DEFAULT_GET_RESPONSE_TYPE;
}
return new Promise((resolve, reject) => {
if (signal?.aborted) return reject(new DOMException("Aborted", "AbortError"));
const {url: targetUrl} = options;
let xhrHandler;
const handleAbort = () => xhrHandler?.abort();
const finalize = (settleFn, ...args) => {
signal?.removeEventListener("abort", handleAbort);
settleFn(...args);
};
xhrHandler = GM_xmlhttpRequest({
onabort: () => reject(new DOMException("Aborted", "AbortError")),
ontimeout: () => finalize(reject, new Error(`Timeout: ${targetUrl}`)),
onerror: () => finalize(reject, new Error(`Network Error: ${targetUrl}`)),
onload: ({
status,
finalUrl,
response
}) => (status >= 400 ? finalize(reject, new Error(`HTTP ${status}: ${targetUrl}`)) : finalize(resolve, method === "HEAD" ? finalUrl : response)),
...options,
params: undefined,
});
signal?.addEventListener("abort", handleAbort, {once: true});
});
}
static async tasks(initial, steps, signal) {
let ctx = initial;
for (const step of steps) {
if (signal?.aborted) throw new DOMException("Aborted", "AbortError");
if (typeof ctx === "string" || ctx?.url) ctx = await this.request(typeof ctx === "string" ? {
url: ctx,
signal
} : {...ctx, signal});
ctx = await step(ctx);
}
return ctx;
}
}
class Drive115 extends Req {
static DEFAULT_GET_RESPONSE_TYPE = "json";
static files(cid = "0", params = {}) {
return this.request({
url: "https://webapi.115.com/files",
params: {cid, ...params},
});
}
static filesSearch(search_value, params = {}) {
return this.request({
url: "https://webapi.115.com/files/search",
params: {search_value, ...params},
});
}
static lixianTaskLists(page = 1) {
return this.request({
url: "https://115.com/web/lixian/",
params: {ct: "lixian", ac: "task_lists", page},
});
}
static labelList() {
return this.request({
url: "https://webapi.115.com/label/list",
params: {keyword: "", limit: 11150},
});
}
/**
* @connect 115vod.com
*/
static filesVideo(pickcode) {
return this.request({
url: "https://115vod.com/webapi/files/video",
params: {pickcode, local: 1},
});
}
static post(details) {
return this.request({method: "POST", ...details});
}
static filesAdd(cname, pid) {
return this.post({
url: "https://webapi.115.com/files/add",
data: {cname, pid},
});
}
static lixianAddTaskUrl(url, wp_path_id) {
return this.post({
url: "https://115.com/web/lixian/",
params: {ct: "lixian", ac: "add_task_url"},
data: {url, wp_path_id},
});
}
/**
* Bulk delete offline tasks and source files
* @param {string[]} hash Array of info_hashes
*/
static lixianTaskDel(hash) {
return this.post({
url: "https://115.com/web/lixian/",
params: {ct: "lixian", ac: "task_del"},
data: {hash},
});
}
/**
* Bulk delete files
* @param {string[]} fid Array of file IDs
* @param {string} pid Parent folder ID
*/
static rbDelete(fid, pid = "") {
return this.post({
url: "https://webapi.115.com/rb/delete",
data: {fid, pid, ignore_warn: 1},
});
}
/**
* Batch move files
* @param {string[]} fid Array of file IDs
* @param {string} pid Destination folder ID
*/
static filesMove(fid, pid) {
return this.post({
url: "https://webapi.115.com/files/move",
data: {fid, pid, move_proid: ""},
});
}
/**
* Bulk label files
* @param {string} file_ids fid1,fid2,fid3...
* @param {string} file_label label_id1,label_id2,label_id3...
*/
static filesBatchLabel(file_ids, file_label, action = "add") {
return this.post({
url: "https://webapi.115.com/files/batch_label",
data: {file_ids, file_label, action},
});
}
/**
* Bulk rename files
* @param {object} files_new_name { [fid]: rename }
*/
static filesBatchRename(files_new_name) {
return this.post({
url: "https://webapi.115.com/files/batch_rename",
data: {files_new_name},
});
}
static sampleInitUpload({filename, filesize, cid}) {
return this.post({
url: "https://uplb.115.com/3.0/sampleinitupload.php",
data: {filename, filesize, target: `U_1_${cid}`},
});
}
/**
* @connect aliyuncs.com
*/
static upload({
host: url,
filename: name,
object: key,
policy,
accessid: OSSAccessKeyId,
callback,
signature,
file,
}) {
return this.post({
url,
data: {
name,
key,
policy,
OSSAccessKeyId,
success_action_status: "200",
callback,
signature,
file,
},
});
}
static filesEdit(fid, fid_cover) {
return this.post({
url: "https://webapi.115.com/files/edit",
data: {fid, fid_cover},
});
}
}
export class Req115 extends Drive115 {
static async filesAll(cid, params = {}) {
const res = await this.files(cid, params);
const {count, page_size, data} = res;
return count > page_size && data.length ? this.files(cid, {...params, limit: count}) : res;
}
static filesAllVideos(cid, params = {}) {
return this.filesAll(cid, {...params, type: 4});
}
static filesAllSRTs(cid, params = {}) {
return this.filesAll(cid, {...params, suffix: "srt"});
}
static async filesSearchAll(search_value, params = {}) {
const res = await this.filesSearch(search_value, params);
const {count, page_size, data} = res;
return count > page_size && data.length ? this.filesSearch(search_value, {...params, limit: count}) : res;
}
static filesSearchAllVideos(search_value, params = {}) {
return this.filesSearchAll(search_value, {...params, type: 4});
}
static filesSearchAllFolders(search_value, params = {}) {
return this.filesSearchAll(search_value, {...params, fc: 1});
}
static async handleDir(routes) {
if (routes.length === 1 && /^\d{5,}$/.test(routes[0])) return routes[0];
let cid;
const routesStr = routes.join("/");
const cachedCid = localStorage.getItem(routesStr);
if (cachedCid) {
const res = await this.files(cachedCid);
if (res?.path?.length) {
const path = res.path.slice(1).map((p) => p.name);
if (path.join("/") === routesStr) cid = cachedCid;
}
}
if (!cid) {
cid = "0";
for (const route of routes) {
const {data} = await this.filesSearchAllFolders(route, {cid});
let folder = data.find((folder) => folder.n === route);
if (!folder) folder = await this.filesAdd(route, cid);
cid = folder?.cid;
if (!cid) break;
}
}
const month = new Date().getMonth().toString();
if (localStorage.getItem("115_CD") !== month) {
localStorage.clear();
localStorage.setItem("115_CD", month);
}
if (cid) localStorage.setItem(routesStr, cid);
return cid;
}
static async handleVerify(info_hash, {regex, codes}, {max, filter}) {
const sleep = () => {
return new Promise((r) => {
setTimeout(r, 1000);
});
};
let file_id = "";
let videos = [];
for (let index = 0; index < max; index++) {
if (index) await sleep();
const {tasks} = await this.lixianTaskLists();
const task = tasks.find((task) => task.info_hash === info_hash);
if (!task || task.status === -1) break;
file_id = task.file_id;
if (file_id) break;
}
if (!file_id) return {file_id, videos};
for (let index = 0; index < max; index++) {
if (index) await sleep();
const {data} = await this.filesAllVideos(file_id);
videos = data.filter((item) => regex.test(item.n));
if (videos.length) break;
}
if (!videos.length) {
const {tasks} = await this.lixianTaskLists();
const task = tasks.find((task) => task.info_hash === info_hash);
if (task.status === 2) {
const {data} = await this.filesAllVideos(file_id);
codes = codes.map((code) => code.toUpperCase());
videos = data.filter((item) => {
const name = item.n.toUpperCase();
return codes.some((code) => name.includes(code));
});
}
}
return {videos: videos.filter(filter), file_id};
}
static async handleClean(keepFiles, cid) {
const needMove = keepFiles.filter((file) => file.cid !== cid).map((file) => file.fid);
if (needMove.length) await this.filesMove(needMove, cid);
const {data} = await this.filesAll(cid);
const needRemove = data
.filter((item) => !keepFiles.some((file) => file.fid === item.fid))
.map((item) => item.fid ?? item.cid);
if (needRemove.length) return this.rbDelete(needRemove, cid);
}
static async handleTags(files, tags) {
const {data} = await this.labelList();
if (!data?.list?.length) return;
const {list} = data;
const labels = [];
tags.forEach((tag) => {
const item = list.find((item) => item.name === tag);
if (item) labels.push(item.id);
});
if (labels.length) return this.filesBatchLabel(files.map((it) => it.fid).toString(), labels.toString());
}
static handleRename(files, cid, {rename, renameTxt, zh, crack}) {
rename = rename.replaceAll("$zh", zh ? renameTxt.zh : "");
rename = rename.replaceAll("$crack", crack ? renameTxt.crack : "");
rename = rename.split("$sep").filter(Boolean).join(renameTxt.sep);
rename = rename.trim();
const renameObj = {[cid]: rename};
if (files.length === 1) {
const {fid, ico} = files[0];
renameObj[fid] = `${rename}.${ico}`;
return this.filesBatchRename(renameObj);
}
const icoMap = files.reduce((acc, {ico, ...item}) => {
acc[ico] ??= [];
acc[ico].push(item);
return acc;
}, {});
const noTxt = renameTxt.no;
for (const [ico, items] of Object.entries(icoMap)) {
if (items.length === 1) {
renameObj[items[0].fid] = `${rename}.${ico}`;
continue;
}
items
.toSorted((a, b) => a.n.localeCompare(b.n))
.forEach(({fid}, idx) => {
const no = noTxt.replaceAll(`\${no}`, `${idx + 1}`.padStart(2, "0"));
renameObj[fid] = `${rename}${no}.${ico}`;
});
}
return this.filesBatchRename(renameObj);
}
static async handleCover(url, cid, filename) {
const file = await this.request({url, timeout: 60000, responseType: "blob"});
if (!file) return;
const res = await this.sampleInitUpload({cid, filename, filesize: file.size});
if (res?.host) return this.upload({...res, filename, file});
}
static async handleOffline(
{dir, regex, codes, verifyOptions, code, rename, renameTxt, tags, clean, cover},
magnets,
) {
const res = {status: "error", msg: `获取目录失败: ${dir.join("/")}`};
const cid = await this.handleDir(dir);
if (!cid) return res;
for (let index = 0, {length} = magnets; index < length; index++) {
const {url, zh, crack} = magnets[index];
const {state, error_msg, errcode, info_hash} = await this.lixianAddTaskUrl(url, cid);
if (!state) {
res.msg = error_msg;
res.status = "error";
res.currIdx = index;
if (errcode === 10008) continue;
if (errcode === 911) res.status = "warn";
break;
}
const {videos, file_id} = await this.handleVerify(info_hash, {regex, codes}, verifyOptions);
if (!videos.length) {
if (verifyOptions.clean) this.lixianTaskDel([info_hash]);
if (file_id && clean) this.rbDelete([file_id], cid);
res.msg = `${code} 离线验证失败`;
res.status = "error";
continue;
}
const {data: srts = []} = await this.filesAllSRTs(file_id);
const files = [...videos, ...srts];
if (clean) await this.handleClean(files, file_id);
if (tags.length) this.handleTags(videos, tags);
if (rename) this.handleRename(files, file_id, {rename, renameTxt, zh: zh || srts.length, crack});
if (cover) {
try {
const {data} = await this.handleCover(cover, file_id, `${code}.cover.jpg`);
if (data?.file_id) this.filesEdit(file_id, data.file_id);
} catch (err) {
console.warn("[Req115.handleCover]", err?.message);
}
}
res.msg = `${code} 离线任务成功`;
res.status = "success";
break;
}
return res;
}
static async queueMutation(name, fn) {
return fn();
}
}
window.Req115 = Req115;
+454 -115
View File
@@ -1,153 +1,492 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb } from '../core/constants.js';
import { searchFiles } from './wangpan-115-task.js';
const Thumbnail = {
sources: ['javfree', 'projectjav', 'javstore'],
cacheKey(code) {
return `thumb_cache_v3_${code}`;
},
lookupCode(code) {
const text = String(code || '').trim();
const fc2 = text.match(/^(?:FC2[-_\s]?(?:PPV[-_\s]?)?)?(\d{6,9})$/i);
return fc2 ? fc2[1] : text;
},
sourceOrder() {
const savedOrder = ['javfree', 'projectjav', 'javstore'];
const seen = new Set();
return [...savedOrder, ...this.sources].filter(src => {
if (seen.has(src) || typeof this[src] !== 'function') return false;
seen.add(src);
return true;
});
},
async fetchFromSource(source, code) {
try {
return await this[source](this.lookupCode(code));
} catch (e) {
clog.debug(`Thumbnail[${source}] 异常:`, e.message);
return null;
}
},
normalizeForCompare(text) {
return String(text || '').toLowerCase().replace(/[^a-z0-9]/g, '');
},
isCodeMatched(text, code) {
const normalizedText = this.normalizeForCompare(text);
const normalizedCode = this.normalizeForCompare(code);
return !!normalizedCode && normalizedText.includes(normalizedCode);
},
isDetailMatched(doc, url, code) {
const title = doc?.querySelector('title')?.textContent || '';
const headings = [...(doc?.querySelectorAll('h1,h2,h3,.entry-title,.movie-title,.post-title') || [])]
.map(el => el.textContent || '').join(' ');
const bodyText = (doc?.body?.textContent || '').slice(0, 5000);
return this.isCodeMatched([url, title, headings, bodyText].join(' '), code);
},
normalizePreviewUrl(url, baseUrl = '') {
if (!url) return '';
const absolute = /^https?:\/\//i.test(url)
? url : (baseUrl ? new URL(url, baseUrl).href : url);
return absolute.replace(/^http:/, 'https:');
},
isJavfreePreviewImage(url, code) {
const cleanUrl = String(url || '').split('?')[0];
const lookupCode = this.lookupCode(code);
const isFc2Numeric = /^\d{6,9}$/.test(lookupCode);
const fc2ShotPattern = isFc2Numeric
? new RegExp(`${lookupCode}_\\d+\\.(?:jpe?g|png|webp)$`, 'i')
: null;
return this.isCodeMatched(cleanUrl, code) && (
/-(?:1080p|demosaic)\.(?:jpe?g|png|webp)$/i.test(cleanUrl) ||
(isFc2Numeric && fc2ShotPattern.test(cleanUrl))
);
},
selectJavfreePreviewUrl(doc, detailUrl, code) {
const urls = [...doc.querySelectorAll('p > img[src]')]
.map(img => this.normalizePreviewUrl(img.getAttribute('src') || img.src || '', detailUrl))
.filter(url => this.isJavfreePreviewImage(url, code));
return urls.find(url => /-1080p\./i.test(url)) ||
urls.find(url => /-demosaic\./i.test(url)) ||
urls.find(url => /_1\.(?:jpe?g|png|webp)$/i.test(url)) || '';
},
async javfree(code) {
code = this.lookupCode(code);
const cacheKey = this.cacheKey(code);
const cached = sessionStorage.getItem(cacheKey);
if (cached) return cached;
try {
const html = await gmHttp.get(`https://javfree.me/search/${code}`);
const doc = new DOMParser().parseFromString(html, 'text/html');
const link = [...doc.querySelectorAll('.entry-title>a')]
.find(a => this.isCodeMatched([a.href, a.textContent].join(' '), code))?.href;
if (!link) return null;
const dHtml = await gmHttp.get(link);
const dDoc = new DOMParser().parseFromString(dHtml, 'text/html');
if (!this.isDetailMatched(dDoc, link, code)) return null;
const url = this.selectJavfreePreviewUrl(dDoc, link, code);
if (url) {
sessionStorage.setItem(cacheKey, url);
return url;
}
return null;
} catch {
return null;
}
},
async javstore(code) {
code = this.lookupCode(code);
try {
const normalizedCode = code.replace(/^fc2-?/i, '').replace(/-/g, '').toLowerCase();
const searchUrl = `https://javstore.net/search?q=${encodeURIComponent(code)}`;
const searchHtml = await gmHttp.get(searchUrl);
const searchDoc = new DOMParser().parseFromString(searchHtml, 'text/html');
const candidateLinks = searchDoc.querySelectorAll('a[href*="/"]');
const detailUrls = [];
for (const link of candidateLinks) {
const href = link.getAttribute('href');
if (!href) continue;
if (href.startsWith('http') && !href.includes('javstore.net')) continue;
const urlObj = new URL(href, searchUrl);
if (!/javstore\.net$/i.test(urlObj.hostname)) continue;
if (/^\/search(?:[/?#]|$)/i.test(urlObj.pathname)) continue;
const fullUrl = urlObj.href;
const pathLastPart = decodeURIComponent(urlObj.pathname.split('/').pop() || '');
const normalizedPath = pathLastPart.toLowerCase().replace(/-/g, '');
const looksLikeDetail = /\.html$/i.test(urlObj.pathname) || /^\/\d+[-/]/.test(urlObj.pathname);
if (looksLikeDetail && normalizedPath.includes(normalizedCode) && !detailUrls.includes(fullUrl)) {
detailUrls.push(fullUrl);
}
}
if (detailUrls.length === 0) return null;
for (const detailUrl of detailUrls) {
const imgUrl = await this._extractImgFromDetail(detailUrl, code);
if (imgUrl) return imgUrl;
}
return null;
} catch (e) {
clog.debug('javstore 获取失败', e);
return null;
}
},
async _extractImgFromDetail(detailUrl, code) {
try {
const detailHtml = await gmHttp.get(detailUrl);
const detailDoc = new DOMParser().parseFromString(detailHtml, 'text/html');
if (!this.isDetailMatched(detailDoc, detailUrl, code)) return null;
for (const link of detailDoc.querySelectorAll('a')) {
if (link.textContent.includes('CLICK HERE')) {
const imgUrl = link.href || link.getAttribute('href') || '';
if (imgUrl) return this.normalizePreviewUrl(imgUrl, detailUrl);
}
}
const img = detailDoc.querySelector('img[src*="_s.jpg"]');
if (img) {
let src = img.getAttribute('src') || '';
if (!src.startsWith('http')) src = new URL(src, detailUrl).href;
return this.normalizePreviewUrl(src.replace(/_s\.jpg$/, '_l.jpg'), detailUrl);
}
return null;
} catch (e) {
clog.debug('javstore: 详情页请求失败', detailUrl, e.message);
return null;
}
},
async projectjav(code) {
code = this.lookupCode(code);
try {
const request = (url) => gmHttp.get(url, {}, {
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8'
});
const searchUrl = `https://projectjav.com/?searchTerm=${encodeURIComponent(code)}`;
const searchHtml = await request(searchUrl);
const searchDoc = new DOMParser().parseFromString(searchHtml, 'text/html');
let detailUrl = '';
const allMovieLinks = [...searchDoc.querySelectorAll('a[href*="/movie/"]')];
const firstLink = allMovieLinks[0]?.getAttribute('href') || '';
if (firstLink) {
detailUrl = firstLink.startsWith('http') ? firstLink : `https://projectjav.com${firstLink}`;
}
if (!detailUrl) return null;
const detailHtml = await request(detailUrl);
const detailDoc = new DOMParser().parseFromString(detailHtml, 'text/html');
const screenshotLink = [...detailDoc.querySelectorAll('.col-md-12.thumbnail a[data-featherlight="image"], .thumbnail a[data-featherlight="image"]')]
.find(a => this.isCodeMatched([a.outerHTML, a.closest('.thumbnail')?.outerHTML, detailUrl].join(' '), code));
if (screenshotLink) {
const thumbImg = screenshotLink.querySelector('img');
const href = screenshotLink.getAttribute('href') || '';
if (href) return this.normalizePreviewUrl(href, detailUrl);
if (thumbImg) {
const src = (thumbImg.getAttribute('src') || '').replace(/\?.*$/, '');
if (src) return this.normalizePreviewUrl(src, detailUrl);
}
}
return null;
} catch (e) {
clog.debug('[projectjav] 异常:', e.message);
return null;
}
},
async get(code) {
const cacheKey = this.cacheKey(code);
const cached = sessionStorage.getItem(cacheKey);
if (cached) return {url: cached, source: null};
let url = null, source = null;
for (const src of this.sourceOrder()) {
url = await this.fetchFromSource(src, code);
if (url) {
source = src;
break;
}
}
if (url) sessionStorage.setItem(cacheKey, url);
return {url, source};
}
};
let previewOverlay = null;
function showPreviewOverlay(imgUrl, code, source = null) {
if (previewOverlay) {
previewOverlay.close();
}
let currentBlobUrl = null;
const originalHtmlOverflow = document.documentElement.style.overflow;
const originalBodyOverflow = document.body.style.overflow;
document.documentElement.style.overflow = 'hidden';
document.body.style.overflow = 'hidden';
const container = document.createElement('div');
container.className = 'jhs-preview-overlay';
const img = document.createElement('img');
img.src = imgUrl;
img.className = 'jhs-preview-img';
img.onclick = (e) => {
e.stopPropagation();
img.classList.toggle('zoomed');
};
img.onerror = () => {
img.src = imgUrl;
};
const toolbar = document.createElement('div');
toolbar.className = 'jhs-preview-toolbar';
const createButton = (text, className, onClick) => {
const btn = document.createElement('button');
btn.className = `jhs-preview-btn ${className}`;
btn.textContent = text;
btn.onclick = onClick;
return btn;
};
const setActiveSource = (activeSource) => {
toolbar.querySelectorAll('.jhs-preview-btn').forEach(b => b.classList.remove('active'));
const activeBtn = toolbar.querySelector(`.jhs-preview-btn-${activeSource}`);
if (activeBtn) activeBtn.classList.add('active');
};
const changeSource = async (src, srcName) => {
const loadingOverlay = document.createElement('div');
loadingOverlay.className = 'jhs-loading-overlay';
loadingOverlay.innerHTML = '<div class="jhs-loading-spinner"><div class="spinner"></div><div>正在加载预览图...</div></div>';
document.body.appendChild(loadingOverlay);
try {
const newUrl = src === 'javfree' ? await Thumbnail.javfree(code)
: src === 'projectjav' ? await Thumbnail.projectjav(code)
: await Thumbnail.javstore(code);
if (newUrl) {
if (currentBlobUrl) {
URL.revokeObjectURL(currentBlobUrl);
currentBlobUrl = null;
}
img.src = newUrl;
setActiveSource(src);
} else {
showToast(`${srcName} 未找到预览图`, 'error');
}
} catch (err) {
showToast(`${srcName} 加载失败: ${err.message}`, 'error');
} finally {
loadingOverlay.remove();
}
};
let toastTimer = null;
const showToast = (msg, type = 'info') => {
const existing = container.querySelector('.jhs-preview-toast');
if (existing) existing.remove();
if (toastTimer) clearTimeout(toastTimer);
const toast = document.createElement('div');
toast.className = `jhs-preview-toast jhs-preview-toast-${type}`;
toast.textContent = msg;
container.appendChild(toast);
requestAnimationFrame(() => toast.classList.add('show'));
toastTimer = setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
}, 3000);
};
const javfreeBtn = createButton('javfree', 'jhs-preview-btn-javfree javfree', (e) => {
e.stopPropagation();
changeSource('javfree', 'javfree');
});
const projectjavBtn = createButton('projectjav', 'jhs-preview-btn-projectjav projectjav', (e) => {
e.stopPropagation();
changeSource('projectjav', 'projectjav');
});
const javstoreBtn = createButton('javstore', 'jhs-preview-btn-javstore javstore', (e) => {
e.stopPropagation();
changeSource('javstore', 'javstore');
});
const newWindowBtn = createButton('新窗口打开', 'jhs-preview-btn-action action', (e) => {
e.stopPropagation();
window.open(img.src);
});
const downloadBtn = createButton('下载', 'jhs-preview-btn-action action', (e) => {
e.stopPropagation();
const a = document.createElement('a');
a.href = img.src;
a.download = `${code}.jpg`;
a.click();
});
toolbar.appendChild(javfreeBtn);
toolbar.appendChild(projectjavBtn);
toolbar.appendChild(javstoreBtn);
toolbar.appendChild(newWindowBtn);
toolbar.appendChild(downloadBtn);
if (source === 'javfree') setActiveSource('javfree');
else if (source === 'projectjav') setActiveSource('projectjav');
else if (source === 'javstore') setActiveSource('javstore');
else setActiveSource('javfree');
container.appendChild(img);
const closeOverlay = () => {
if (container.parentNode) {
container.remove();
toolbar.remove();
document.documentElement.style.overflow = originalHtmlOverflow;
document.body.style.overflow = originalBodyOverflow;
if (currentBlobUrl) {
URL.revokeObjectURL(currentBlobUrl);
currentBlobUrl = null;
}
}
};
container.onclick = closeOverlay;
const escHandler = (e) => {
if (e.key === 'Escape') {
closeOverlay();
document.removeEventListener('keydown', escHandler);
}
};
document.addEventListener('keydown', escHandler);
document.body.appendChild(container);
document.body.appendChild(toolbar);
previewOverlay = {container, toolbar, close: closeOverlay};
}
class ScreenShotPlugin extends BasePlugin {
getName() {
return "ScreenShotPlugin";
}
async handle() {
this.loadScreenShot().then();
}
async loadScreenShot() {
if (!window.isDetailPage) return;
if ("yes" !== await storageManager.getSetting("enableLoadScreenShot", "yes")) return;
let carNum2 = this.getPageInfo().carNum;
isJavDb && $(".preview-images .tile-item").first().before(' <a class="tile-item screen-container" style="overflow:hidden;max-height: 215px;text-align:center;"><div style="margin-top: 50px;color: #000;cursor: auto">正在加载缩略图</div></a> ');
isJavBus && $("#sample-waterfall .sample-box:first").after(' <a class="sample-box screen-container" style="overflow:hidden; height: 110px; text-align:center;"><div style="margin-top: 30px;color: #000;cursor: auto">正在加载缩略图</div></a> ');
isJavDb && $(".preview-images .tile-item").first().before(
'<a class="tile-item screen-container" style="overflow:hidden;max-height:215px;text-align:center;">' +
'<div style="margin-top:50px;color:#000;cursor:auto">正在加载缩略图</div></a>'
);
isJavBus && $("#sample-waterfall .sample-box:first").after(
'<a class="sample-box screen-container" style="overflow:hidden;height:110px;text-align:center;">' +
'<div style="margin-top:30px;color:#000;cursor:auto">正在加载缩略图</div></a>'
);
try {
const imgUrl = await this.getScreenshot(carNum2);
this.addImg("缩略图", imgUrl);
clog.log("加载缩略图:", imgUrl);
const result = await Thumbnail.get(carNum2);
if (result.url) {
this.addImg("缩略图", result.url, carNum2, result.source);
clog.log("加载缩略图:", result.url, result.source);
} else {
this.showErrorFallback(carNum2, new Error("所有来源均未找到预览图"));
}
} catch (e) {
this.showErrorFallback(carNum2, e);
}
}
async getScreenshot(carNum2) {
const cacheData = localStorage.getItem("jhs_screenShot") ? JSON.parse(localStorage.getItem("jhs_screenShot")) : {};
if (cacheData[carNum2]) {
clog.debug("缓存中存在缩略图:", carNum2, cacheData[carNum2]);
return cacheData[carNum2];
}
let imgUrl;
try {
imgUrl = await Promise.any([ this.getJavStoreScreenShot(carNum2) ]);
} catch (e) {
clog.error("获取缩略图资源失败:", imgUrl, e);
throw e;
}
if (!imgUrl) {
this.showErrorFallback(carNum2, null);
return null;
}
const httpsIndex = imgUrl.indexOf("https://");
-1 !== httpsIndex && (imgUrl = imgUrl.substring(httpsIndex));
cacheData[carNum2] = imgUrl;
clog.log("缩略图获取成功:", imgUrl);
localStorage.setItem("jhs_screenShot", JSON.stringify(cacheData));
return imgUrl;
const result = await Thumbnail.get(carNum2);
if (result.url) return result.url;
throw new Error("未找到预览图");
}
async getJavStoreScreenShot(carNum2) {
let url = `https://javstore.net/search/${carNum2}.html`;
clog.log("正在解析缩略图:", url);
let html = await gmHttp.get(url);
const $dom = utils.htmlTo$dom(html);
let detailPageUrl = null;
$dom.find("#content_news h3 span a").each((function() {
if ($(this).attr("title").toLowerCase().includes(carNum2.toLowerCase())) {
detailPageUrl = $(this).attr("href");
return !1;
}
}));
if (!detailPageUrl) {
clog.error("JavStore, 查询番号失败:", url);
return null;
}
let detailPageHtml = await gmHttp.get(detailPageUrl);
const $detailPageDom = utils.htmlTo$dom(detailPageHtml);
let imgUrl = $detailPageDom.find("a:contains('CLICK HERE')").attr("href") || $detailPageDom.find("img[src*='_s.jpg']").attr("src");
if (!imgUrl) {
clog.error("JavStore, 解析预览图失败:", url);
return null;
}
return imgUrl.replace(".th", "");
}
async getJavBestScreenShot(carNum2) {
let url = `https://javbest.net/?s=${carNum2}`;
clog.log("正在解析缩略图:", url);
let html = await gmHttp.get(url);
const $dom = utils.htmlTo$dom(html), href = $dom.find(".app_loop_thumb a").first().attr("href");
if (!href) {
clog.error("解析JavBest搜索页失败:", url);
throw new Error("解析JavBest搜索页失败");
}
const title = $dom.find(".app_loop_thumb a").first().attr("title");
if (!title.toLowerCase().includes(carNum2.toLowerCase())) {
clog.error("解析JavBest搜索页失败:", title);
throw new Error("解析JavBest搜索页失败");
}
const detailPageHtml = await gmHttp.get(href);
let imgUrl = $(detailPageHtml).find('#content a img[src*="_t.jpg"]').attr("src");
if (!imgUrl) {
clog.error("解析JavBest缩略图失败:", url);
throw new Error("解析JavBest缩略图失败");
}
imgUrl = imgUrl.replace("_t", "").replace("http:", "https:");
return imgUrl;
return await Thumbnail.javstore(carNum2);
}
async getJavFreeScreenShot(carNum2) {
let url = `https://javfree.me/search/${carNum2}/`, html = await gmHttp.get(url);
const $itemList = utils.htmlTo$dom(html).find("article h2.entry-title a");
if (!$itemList || 0 === $itemList.length) {
clog.error("解析JavFree搜索页失败:", url);
throw new Error("解析JavFree搜索页失败");
}
let detailPageUrl = $($itemList[0]).attr("href"), detailPageHtml = await gmHttp.get(detailPageUrl);
const $imgList = utils.htmlTo$dom(detailPageHtml).find("#main > article > .entry-content > p img");
if (!$imgList || 0 === $imgList.length) {
clog.error("解析JavFree详情页失败:", detailPageUrl);
throw new Error("解析JavFree详情页失败");
}
const srcList = $imgList.filter((function() {
const src = $(this).attr("src");
return src && src.toLowerCase().endsWith(".jpeg");
})).map((function() {
return $(this).attr("src");
})).get();
console.log(srcList);
return srcList.at(-1);
return await Thumbnail.javfree(carNum2);
}
addImg(title, imgUrl) {
if (imgUrl) {
isJavDb && $(".screen-container").html(`<img src="${imgUrl}" alt="${title}" loading="lazy" style="width: 100%;">`);
isJavBus && $(".screen-container").html(`<div class="photo-frame"><img src="${imgUrl}" style="height: inherit;width: 100%;" title="${title}" alt="${title}"></div>`);
$(".screen-container").on("click", (event => {
event.stopPropagation();
event.preventDefault();
showImageViewer(event.currentTarget);
}));
}
addImg(title, imgUrl, code, source) {
if (!imgUrl) return;
isJavDb && $(".screen-container").html(
`<img src="${imgUrl}" alt="${title}" loading="lazy" style="width:100%;">`
);
isJavBus && $(".screen-container").html(
`<div class="photo-frame"><img src="${imgUrl}" style="height:inherit;width:100%;" title="${title}" alt="${title}"></div>`
);
$(".screen-container").on("click", (event) => {
event.stopPropagation();
event.preventDefault();
showPreviewOverlay(imgUrl, code || title, source);
});
}
showErrorFallback(carNum2, error) {
var _a2;
console.error("获取缩略图失败:", null == (_a2 = null == error ? void 0 : error.message) ? void 0 : _a2.substring(0, 100));
let differentCss = isJavBus ? "margin-top: 30px" : "margin-top: 50px";
$(".screen-container").html(`<div style="${differentCss}; cursor:auto;color:#000;">获取缩略图失败</div><br/><a href='#' class='retry-link'>点击重试</a> 或 <a class="check-link" href='https://javstore.net/search/${carNum2}.html' target='_blank'>前往确认</a>`).off("click", ".retry-link").off("click", ".check-link").on("click", ".retry-link", (async e => {
console.error("获取缩略图失败:", error?.message?.substring?.(0, 100) || error);
let differentCss = isJavBus ? "margin-top:30px" : "margin-top:50px";
$(".screen-container").html(
`<div style="${differentCss};cursor:auto;color:#000;">获取缩略图失败</div>` +
`<br/><a href='#' class='retry-link'>点击重试</a> 或 ` +
`<a class="check-link" href='https://javstore.net/search?q=${encodeURIComponent(carNum2)}' target='_blank'>前往确认</a>`
).off("click", ".retry-link").off("click", ".check-link")
.on("click", ".retry-link", async (e) => {
e.stopPropagation();
e.preventDefault();
$(".screen-container").html(`<div style="${differentCss};cursor:auto;color:#000;">正在重新加载...</div>`);
$(".screen-container").html(
`<div style="${differentCss};cursor:auto;color:#000;">正在重新加载...</div>`
);
try {
const imgUrl = await this.getScreenshot(carNum2);
this.addImg("缩略图", imgUrl);
const result = await Thumbnail.get(carNum2);
if (result.url) {
this.addImg("缩略图", result.url, carNum2, result.source);
} else {
this.showErrorFallback(carNum2, new Error("所有来源均未找到预览图"));
}
} catch (err) {
this.showErrorFallback(carNum2, err);
}
})).on("click", ".check-link", (async e => {
}).on("click", ".check-link", (e) => {
e.stopPropagation();
e.preventDefault();
window.open(`https://javstore.net/search/${carNum2}.html`, "_blank");
}));
window.open(`https://javstore.net/search?q=${encodeURIComponent(carNum2)}`, "_blank");
});
}
}
export { ScreenShotPlugin };
export {ScreenShotPlugin, Thumbnail, showPreviewOverlay};
window.showPreviewOverlay = showPreviewOverlay;
+159 -8
View File
File diff suppressed because one or more lines are too long
+605
View File
@@ -0,0 +1,605 @@
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
/**
* @name JavPack Subtitle Library
* @description Xunlei and SubtitleCat subtitle search, preview, download, and 115 upload for JavDB.
*/
export class JavPackSubtitle {
static BTN_ID = "x-subtitle-search-btn";
static MODAL_ID = "x-subtitle-modal";
static previewCache = new Map();
static cached115Matches = [];
static current115Cids = [];
static last115Code = "";
static SUBTITLECAT_ORIGIN = "https://www.subtitlecat.com";
static SUBTITLECAT_CHINESE_LANGUAGES = new Map([
["zh-CN", "简中"],
["zh-TW", "繁中"],
]);
static escapeHtml(value = "") {
return String(value)
.replaceAll("&", "&amp;")
.replaceAll("<", "&lt;")
.replaceAll(">", "&gt;")
.replaceAll('"', "&quot;");
}
static sanitizeName(value = "") {
return String(value).replace(/[\\/:*?"<>|]/g, "").replace(/\s+/g, " ").trim();
}
static isSubtitleFile(file = {}) {
return /\.(srt|ass|ssa|vtt|sub)$/i.test(file.n || "");
}
static buildDefaultKeyword(details = {}) {
return this.sanitizeName(details.code || "") || this.sanitizeName([details.code, details.title].filter(Boolean).join(" ")) || document.title;
}
static buildSearchKeywords(details = {}) {
return [this.buildDefaultKeyword(details)].filter(Boolean);
}
static buildSubtitleBaseName(details = {}) {
if (window.JavPackMatch115Console?.buildRename) {
return window.JavPackMatch115Console.buildRename(details, [{n: `${details.code || "subtitle"}.mp4`}]);
}
return this.sanitizeName([details.code, details.title].filter(Boolean).join(" ")) || this.buildDefaultKeyword(details);
}
static buildSubtitleFilename(details = {}, item = {}) {
const ext = this.sanitizeName(item.ext || "srt").replace(/^\.+/, "") || "srt";
return `${this.buildSubtitleBaseName(details)}.${ext}`;
}
static clearPreviewCache() {
this.previewCache.clear();
}
static async checkSubInCloud(req115, cid) {
if (!req115 || !cid) return false;
const res = await req115.filesAll(cid);
return (res?.data || []).some((file) => this.isSubtitleFile(file));
}
static getReq115() {
return window.Req115 || null;
}
static getTargetCid() {
return document.querySelector(".x-match-cont .zymatch-item [data-cid]")?.dataset.cid
|| document.querySelector(".x-match-cont .zymatch-item")?.dataset.cid
|| "";
}
static async find115Cid(code) {
if (!code) return "";
const req115 = this.getReq115();
if (!req115) return "";
try {
const {data} = await req115.filesSearchAllFolders(code);
if (!data?.length) return "";
const exact = data.find((f) => f.n === code);
if (exact) return exact.cid;
const upper = data.find((f) => f.n.toUpperCase() === code.toUpperCase());
if (upper) return upper.cid;
const contains = data.find((f) => f.n.toUpperCase().includes(code.toUpperCase()));
return contains?.cid || "";
} catch (e) {
console.warn("[JavPackSubtitle] 115搜索目录失败:", e.message);
return "";
}
}
static async preload115Matches(code) {
this.cached115Matches = [];
this.current115Cids = [];
if (!code) return;
const req115 = this.getReq115();
if (!req115) return;
try {
const {data} = await req115.filesSearchAllFolders(code);
if (!data?.length) return;
const codeUpper = code.toUpperCase();
const matches = data
.filter((f) => f.n.toUpperCase().includes(codeUpper))
.map((f) => ({cid: f.cid, name: f.n}))
.sort((a, b) => {
const aExact = a.name.toUpperCase() === codeUpper;
const bExact = b.name.toUpperCase() === codeUpper;
if (aExact && !bExact) return -1;
if (!aExact && bExact) return 1;
return a.name.localeCompare(b.name);
});
this.cached115Matches = matches;
this.current115Cids = matches.length === 1 ? [matches[0].cid] : matches.map((m) => m.cid);
this.last115Code = code;
} catch (e) {
console.warn("[JavPackSubtitle] 115预加载目录失败:", e.message);
}
}
static ensureDetailButton({details, getTargetCid, getSubtitleEntries} = {}) {
const panel = document.querySelector(".movie-panel-info");
const buttons = panel?.querySelector(".panel-block:last-child .buttons");
if (!buttons || document.getElementById(this.BTN_ID)) return;
const btn = document.createElement("button");
btn.id = this.BTN_ID;
btn.type = "button";
btn.className = "button is-small is-info x-subtitle-search";
btn.textContent = "字幕搜索";
const refreshSubtitleFileTip = () => {
const groups = new Map();
(getSubtitleEntries?.() || []).forEach(({directory, files = []}) => {
const path = String(directory || "").trim();
if (!path) return;
if (!groups.has(path)) groups.set(path, new Set());
files.map((file) => String(file?.n || file?.name || file || "").trim())
.filter(Boolean)
.forEach((name) => groups.get(path).add(name));
});
btn.title = groups.size
? ["已有字幕文件:", ...[...groups].flatMap(([path, files]) => [
`目录:${path}`,
...[...files].map((name) => ` - ${name}`),
])].join("\n")
: "暂无已匹配的字幕文件";
btn.textContent = groups.size ? "已有字幕" : "字幕搜索";
btn.classList.toggle("is-success", Boolean(groups.size));
btn.classList.toggle("is-info", !groups.size);
};
btn.addEventListener("mouseenter", refreshSubtitleFileTip);
btn.addEventListener("focus", refreshSubtitleFileTip);
btn.addEventListener("click", (e) => {
e.preventDefault();
e.stopPropagation();
this.openSearchModal({details, getTargetCid});
});
buttons.appendChild(btn);
refreshSubtitleFileTip();
const matchRoot = document.querySelector(".x-match-cont");
if (matchRoot) new MutationObserver(refreshSubtitleFileTip).observe(matchRoot, {
childList: true,
subtree: true,
attributes: true,
attributeFilter: ["data-has-subtitle", "data-subtitle-files"],
});
}
static modalTemplate(defaultKw) {
return `
<div class="pdb-sub-modal">
<div class="pdb-sub-header">
<div class="pdb-sub-search-wrap">
<span class="pdb-sub-title">字幕检索:</span>
<input type="text" id="sub-search-input" value="${this.escapeHtml(defaultKw)}" class="pdb-sub-input" placeholder="输入检索词..." />
<button id="sub-search-btn" class="pdb-sub-btn">重新搜索</button>
</div>
<div class="pdb-sub-115-wrap" id="sub-115-wrap" style="display:none;">
<span class="pdb-sub-title">115目录:</span>
<div id="sub-115-select" class="pdb-sub-checkbox-group"></div>
</div>
<span class="pdb-sub-close" id="sub-close-btn">&times;</span>
</div>
<div class="pdb-sub-body">
<div class="pdb-sub-content"></div>
<div class="pdb-sub-preview-wrap">
<div class="pdb-sub-preview-header">
<span>字幕内容预览</span><span id="preview-status" class="pdb-sub-preview-status">暂无预览</span>
</div>
<textarea class="pdb-sub-textarea" readonly></textarea>
</div>
</div>
</div>
`;
}
static openSearchModal({details = {}, getTargetCid} = {}) {
const defaultKw = this.buildDefaultKeyword(details);
this.currentDetails = details;
document.getElementById(this.MODAL_ID)?.remove();
const overlay = document.createElement("div");
overlay.id = this.MODAL_ID;
overlay.className = "pdb-sub-overlay";
overlay.innerHTML = this.modalTemplate(defaultKw);
document.body.appendChild(overlay);
const contentWrap = overlay.querySelector(".pdb-sub-content");
const previewBox = overlay.querySelector(".pdb-sub-textarea");
const statusNode = overlay.querySelector("#preview-status");
const input = overlay.querySelector("#sub-search-input");
const dirWrap = overlay.querySelector("#sub-115-wrap");
const dirSelect = overlay.querySelector("#sub-115-select");
const refresh115DirUI = () => {
if (!this.cached115Matches.length) {
dirWrap.style.display = "none";
return;
}
dirWrap.style.display = "";
dirSelect.innerHTML = this.cached115Matches.map((m) => {
const checked = this.current115Cids.includes(m.cid) ? " checked" : "";
const displayName = m.name.length > 26 ? m.name.slice(0, 26) + '...' : m.name;
return `<label class="pdb-sub-checkbox-label"><input type="checkbox" class="pdb-sub-checkbox" value="${m.cid}"${checked}><span title="${this.escapeHtml(m.name)}">${this.escapeHtml(displayName)}</span></label>`;
}).join("");
};
dirSelect.addEventListener("change", () => {
this.current115Cids = Array.from(dirSelect.querySelectorAll("input:checked")).map((cb) => cb.value);
});
if (this.last115Code === details.code && this.cached115Matches.length) {
refresh115DirUI();
} else {
this.preload115Matches(details.code).then(refresh115DirUI);
}
const closeModal = () => {
this.clearPreviewCache();
previewBox.value = "";
overlay.remove();
};
overlay.querySelector("#sub-close-btn").addEventListener("click", closeModal);
overlay.addEventListener("click", (e) => {
if (e.target === overlay) closeModal();
});
const performSearch = async (kw) => {
if (!kw) return;
contentWrap.innerHTML = '<div class="pdb-sub-msg">正在搜索字幕来源,请稍候...</div>';
previewBox.value = "";
statusNode.textContent = "暂无预览";
try {
const results = await Promise.allSettled([
this.searchXunlei(kw),
this.searchSubtitleCat(kw),
]);
const dataList = results.flatMap((result) => result.status === "fulfilled" ? result.value : []);
const chineseResults = this.sortResults(dataList, kw, details);
if (!chineseResults.length) {
contentWrap.innerHTML = '<div class="pdb-sub-msg">未找到简中或繁中字幕,请尝试使用完整番号搜索</div>';
return;
}
this.renderTable({
container: contentWrap,
dataList: chineseResults,
previewBox,
statusNode,
overlay,
details,
getTargetCid,
kw
});
if (kw.toUpperCase() !== (details.code || "").toUpperCase()) {
this.preload115Matches(kw).then(refresh115DirUI);
}
} catch (e) {
console.error('字幕搜索失败:', e);
contentWrap.innerHTML = '<div class="pdb-sub-msg">搜索失败,请稍后重试</div>';
}
};
overlay.querySelector("#sub-search-btn").addEventListener("click", () => performSearch(input.value.trim()));
input.addEventListener("keypress", (e) => {
if (e.key === "Enter") performSearch(input.value.trim());
});
performSearch(defaultKw);
}
static requestText(url) {
return window.gmHttp.get(url);
}
static async searchXunlei(kw) {
const root = await this.requestText(`https://api-shoulei-ssl.xunlei.com/oracle/subtitle?name=${encodeURIComponent(kw)}`);
if (!root || root.code !== 0 || !Array.isArray(root.data)) return [];
const results = root.data.slice(0, 20).map((item) => this.normalizeXunleiResult(item));
const labeledResults = await Promise.all(results.map((item) => this.labelXunleiChineseSubtitle(item)));
return labeledResults.filter(Boolean);
}
static normalizeXunleiResult(item = {}) {
const rawLanguages = item.languages ?? item.Languages ?? item.langs ?? item.Langs ?? item.language ?? item.Language ?? [];
return {
...item,
name: item.name || item.Name || item.extra_name || item.Title || "",
languages: Array.isArray(rawLanguages) ? rawLanguages : (rawLanguages ? [String(rawLanguages)] : []),
ext: item.ext || item.Ext || item.format || item.Format || "srt",
url: item.url || item.Url || "",
provider: "迅雷",
};
}
static async labelXunleiChineseSubtitle(item) {
if (this.isWantedChineseSubtitle(item)) return item;
if (!item.url) return null;
try {
const language = this.detectChineseSubtitleLanguage(this.decodeSubtitle(await this.fetchBinaryCached(item.url)));
return language ? {...item, languages: [language]} : null;
} catch (_) {
return null;
}
}
static detectChineseSubtitleLanguage(text = "") {
const sample = String(text).slice(0, 12000);
if (!/[\u3400-\u9fff]/.test(sample)) return "";
const simplifiedCount = (sample.match(/[为与个么开关东风头后发这边过还进时样气应实话说问题现经体万台农国门书车云]/g) || []).length;
const traditionalCount = (sample.match(/[為與個麼開關東風頭後發這邊過還進時樣氣應實話說問題現經體萬臺農國門書車雲]/g) || []).length;
if (simplifiedCount === 0 && traditionalCount === 0) return "";
return traditionalCount > simplifiedCount ? "繁中" : "简中";
}
static async searchSubtitleCat(kw) {
const searchUrl = `${this.SUBTITLECAT_ORIGIN}/index.php?search=${encodeURIComponent(kw)}`;
const searchHtml = await this.requestText(searchUrl);
const entries = this.parseSubtitleCatSearch(searchHtml).slice(0, 20);
const languageGroups = await Promise.all(entries.map(async (entry) => {
try {
return this.parseSubtitleCatLanguages(await this.requestText(entry.detailUrl), entry);
} catch (_) {
return [];
}
}));
return languageGroups.flat();
}
static parseSubtitleCatSearch(html) {
const doc = new DOMParser().parseFromString(html, "text/html");
return [...doc.querySelectorAll("table.sub-table tbody tr")].flatMap((row) => {
const link = row.querySelector('a[href*="subs/"]');
const name = link?.textContent?.trim();
if (!link || !name) return [];
return [{
name,
detailUrl: new URL(link.getAttribute("href"), this.SUBTITLECAT_ORIGIN).href,
}];
});
}
static parseSubtitleCatLanguages(html, entry) {
const doc = new DOMParser().parseFromString(html, "text/html");
return [...doc.querySelectorAll(".sub-single")].flatMap((node) => {
const languageCode = node.querySelector("img.flag")?.getAttribute("alt") || "";
const language = this.SUBTITLECAT_CHINESE_LANGUAGES.get(languageCode);
const link = node.querySelector("a.green-link[href$='.srt']");
if (!language || !link) return [];
return [{
name: entry.name,
languageCode,
languages: [language],
ext: "srt",
provider: "SubtitleCat",
url: new URL(link.getAttribute("href"), this.SUBTITLECAT_ORIGIN).href,
}];
});
}
static isWantedChineseSubtitle(item = {}) {
if (item.provider === "SubtitleCat") return this.SUBTITLECAT_CHINESE_LANGUAGES.has(item.languageCode);
const language = Array.isArray(item.languages) ? item.languages.join(" ") : String(item.languages || "");
const name = `${item.name || ""} ${item.extra_name || ""}`;
return /(zh[-_]?cn|zh[-_]?tw|chs|cht|chinese\s*(simplified|traditional)|简体|繁体|繁體|简中|繁中)/i.test(`${language} ${name}`);
}
static sortResults(dataList, kw = "", details = {}) {
const kwClean = kw.toLowerCase().replace(/[-_.\s]/g, "");
const tokens = kw.toLowerCase().split(/[-_.\s]+/).filter((word) => word.length > 1);
const codeClean = String(details.code || kw).toLowerCase().replace(/[-_.\s]/g, "");
return [...dataList]
.filter((item) => this.isWantedChineseSubtitle(item))
.sort((a, b) => this.scoreResult(b, kwClean, tokens, codeClean) - this.scoreResult(a, kwClean, tokens, codeClean));
}
static scoreResult(item, kwClean, tokens, codeClean = "") {
const name = (item.name || item.extra_name || "").toLowerCase();
const compactName = name.replace(/[-_.\s]/g, "");
const lang = ((item.languages && item.languages[0]) || "").toLowerCase();
let score = 0;
if (codeClean && compactName.includes(codeClean)) score += 800;
tokens.forEach((token) => {
if (name.includes(token)) score += 50;
});
if (kwClean && compactName.includes(kwClean)) score += 500;
if (/(zh|cn|chs|cht|chinese|简中|繁中)/i.test(lang) || /(zh|cn|chs|cht|chinese|简中|繁中)/i.test(name)) score += 100;
if (item.ext === "srt" || item.ext === "ass") score += 20;
return score;
}
static renderTable({container, dataList, previewBox, statusNode, overlay, details, getTargetCid, kw = ""}) {
const words = kw.split(/[-_.\s]+/).filter((word) => word.length > 1);
const highlightRegex = words.length ? new RegExp(`(${words.map((word) => word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})`, "gi") : null;
container.innerHTML = `
<table class="pdb-sub-table">
<thead>
<tr>
<th class="pdb-sub-th" style="width: 90px;">来源</th>
<th class="pdb-sub-th">原始字幕名称</th>
<th class="pdb-sub-th" style="width: 80px;">语言</th>
<th class="pdb-sub-th" style="width: 60px;">格式</th>
<th class="pdb-sub-th" style="width: 170px; text-align:center;">操作</th>
</tr>
</thead>
<tbody>
${dataList.map((item, index) => this.renderRow(item, index, highlightRegex)).join("")}
</tbody>
</table>
`;
container.querySelectorAll(".sub-action-btn").forEach((btn) => {
btn.addEventListener("click", () => this.handleAction({
btn,
item: dataList[btn.dataset.idx],
previewBox,
statusNode,
overlay,
details,
getTargetCid
}));
});
}
static renderRow(item, index, highlightRegex) {
const lang = item.languages?.[0] || "未知";
const name = item.name || item.extra_name || "未知字幕";
const provider = item.provider || "迅雷";
const providerClass = provider === "SubtitleCat" ? "pdb-sub-provider--subtitlecat" : "pdb-sub-provider--xunlei";
const displayName = highlightRegex ? this.escapeHtml(name).replace(highlightRegex, '<span style="color:#e74c3c; font-weight:bold;">$1</span>') : this.escapeHtml(name);
return `
<tr class="pdb-sub-tr">
<td class="pdb-sub-td-provider"><span class="pdb-sub-provider ${providerClass}">${this.escapeHtml(provider)}</span></td>
<td class="pdb-sub-td">${displayName}</td>
<td class="pdb-sub-td-lang">${this.escapeHtml(lang)}</td>
<td class="pdb-sub-td-ext">${this.escapeHtml(item.ext || "srt")}</td>
<td class="pdb-sub-td-actions">
<button class="sub-action-btn pdb-sub-action-btn pdb-sub-btn-preview" data-action="preview" data-idx="${index}">预览</button>
<button class="sub-action-btn pdb-sub-action-btn pdb-sub-btn-download" data-action="download" data-idx="${index}">下载</button>
<button class="sub-action-btn pdb-sub-action-btn pdb-sub-btn-upload" data-action="upload" data-idx="${index}">115直传</button>
</td>
</tr>
`;
}
static async handleAction({btn, item, previewBox, statusNode, overlay, details, getTargetCid}) {
const action = btn.dataset.action;
const oldText = btn.textContent;
const url = item.url;
if (!url) return alert("无效的字幕下载直链");
const filename = this.buildSubtitleFilename(details, item);
btn.textContent = "获取中...";
btn.style.opacity = "0.6";
try {
const buffer = await this.fetchBinaryCached(url);
const text = this.decodeSubtitle(buffer);
if (text.includes("<?xml") && text.includes("<Code>NoSuchKey</Code>")) throw new Error("该字幕在迅雷云端已失效丢失 (NoSuchKey)");
if (action === "preview") {
previewBox.value = text;
statusNode.textContent = filename;
this.setActivePreviewRow(overlay, btn);
} else if (action === "download") {
this.downloadBuffer(buffer, filename);
} else if (action === "upload") {
const jdbCid = getTargetCid?.() || this.getTargetCid();
let cids = jdbCid ? [jdbCid] : [...this.current115Cids];
const req115 = this.getReq115();
if (!cids.length || !req115) throw new Error("未在 115 找到匹配目录,请确认作品已下载到 115");
btn.textContent = "直传中...";
for (const cid of cids) {
await this.uploadSubtitle({req115, cid, filename, buffer});
}
show.ok(`字幕已上传到 115(${cids.length}个目录)`);
const detailBtn = document.getElementById(this.BTN_ID);
if (detailBtn) {
detailBtn.classList.add("is-success");
detailBtn.textContent = "已有字幕";
}
window.dispatchEvent(new CustomEvent("JavDB_SubtitleUploaded", {
detail: {
code: details.code || "",
cid: String(cids[0]),
subtitle: {n: filename, s: buffer.byteLength}
},
}));
overlay.remove();
this.clearPreviewCache();
}
} catch (err) {
alert(`执行中止: ${err.message}`);
} finally {
btn.textContent = oldText;
btn.style.opacity = "1";
}
}
static setActivePreviewRow(overlay, btn) {
overlay.querySelectorAll(".pdb-sub-tr.is-previewing").forEach((row) => {
row.classList.remove("is-previewing");
row.removeAttribute("aria-current");
});
const row = btn.closest(".pdb-sub-tr");
if (!row) return;
row.classList.add("is-previewing");
row.setAttribute("aria-current", "true");
}
static decodeSubtitle(buffer) {
let text = new TextDecoder("utf-8").decode(buffer);
if ((text.match(/\uFFFD/g) || []).length > 3) text = new TextDecoder("gbk").decode(buffer);
return text;
}
static fetchBinaryCached(url) {
if (this.previewCache.has(url)) return Promise.resolve(this.previewCache.get(url));
return this.fetchBinary(url).then((buffer) => {
this.previewCache.set(url, buffer);
return buffer;
});
}
static fetchBinary(url) {
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
method: "GET",
url,
responseType: "arraybuffer",
onload: (res) => {
if (res.status === 200 && res.response) resolve(res.response);
else reject(new Error(`字幕流获取失败,HTTP_CODE: ${res.status}`));
},
onerror: () => reject(new Error("跨域网络请求被阻断")),
});
});
}
static downloadBuffer(buffer, filename) {
const blob = new Blob([buffer], {type: "application/octet-stream"});
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = filename;
link.click();
URL.revokeObjectURL(link.href);
}
static uploadSubtitle(args) {
return args.req115.queueMutation("上传字幕", () => this.uploadSubtitleNow(args));
}
static async uploadSubtitleNow({req115, cid, filename, buffer}) {
const blob = new Blob([buffer], {type: "application/octet-stream"});
const file = new File([blob], filename, {type: "application/octet-stream"});
const initRes = await req115.sampleInitUpload({filename, filesize: file.size, cid});
if (!initRes || (!initRes.host && initRes.status !== 2 && initRes.statuscode !== 0)) {
throw new Error(initRes?.error_msg || "获取115上传安全凭证被拦截");
}
if (!initRes.host) return initRes;
let uploadRes = null;
for (let retry = 0; retry < 3; retry++) {
uploadRes = await req115.upload({...initRes, filename, file});
if (uploadRes && uploadRes.state !== false) return uploadRes;
await new Promise((resolve) => setTimeout(resolve, 1500));
}
throw new Error(uploadRes?.error_msg || uploadRes?.error || "115 服务器拒绝接收回调");
}
};
window.JavPackSubtitle = JavPackSubtitle;
+19 -6
View File
@@ -29,6 +29,9 @@ class TOP250Plugin extends BasePlugin {
$("h2.section-title").contents().first().replaceWith("Top250");
$(".empty-message").remove();
$(".section .container .box").remove();
$(".section .container .movie-list").remove();
$(".section .container .pagination").remove();
$(".section .container select").remove();
$("#sort-toggle-btn").remove();
this.$contentBox.append('<div class="tool-box" style="margin-top: 10px"></div>');
this.$contentBox.append('<div class="movie-list h cols-4 vcols-8" style="margin-top: 10px"></div>');
@@ -56,9 +59,16 @@ class TOP250Plugin extends BasePlugin {
}));
}
async handleTop() {
if (!window.location.href.includes("handleTop=1")) return;
if (!window.location.href.includes("/rankings/top")) return;
const urlParams = new URLSearchParams(window.location.search);
let type = urlParams.get("handleType") || "all", type_value = urlParams.get("type_value") || "";
let t = urlParams.get("t") || "", type = "all", type_value = "";
if (/^y\d+$/.test(t)) {
type = "year";
type_value = t.substring(1);
} else if ("" !== t) {
type = "video_type";
type_value = t;
}
this.has_cnsub = urlParams.get("has_cnsub") || "";
let page = urlParams.get("page") || 1;
this.toolBar(type, type_value, page);
@@ -80,6 +90,7 @@ class TOP250Plugin extends BasePlugin {
const filter_movies = dataList.filter((item => "1" === this.has_cnsub ? item.has_cnsub : "0" !== this.has_cnsub || !item.has_cnsub));
let moviesHtml = javDbApi.markDataListHtml(filter_movies);
$movieBox.html(moviesHtml);
this.pluginManager.getBean("ListPagePlugin").doFilter();
} else {
clog.error(res);
$movieBox.html(`<h3>${message}</h3>`);
@@ -101,9 +112,10 @@ class TOP250Plugin extends BasePlugin {
$(".pagination-list li a").each((function() {
parseInt($(this).text()) > 5 && $(this).closest("li").remove();
}));
const currentTParam = "all" === type ? "" : "year" === type ? `y${type_value}` : type_value;
let yearHtml = "";
for (let year = (new Date).getFullYear(); year >= 2008; year--) yearHtml += `\n <a style="padding:18px 18px !important;" \n class="button is-small ${type_value === year.toString() ? "is-info" : ""}" \n href="/advanced_search?handleTop=1&handleType=year&type_value=${year}&has_cnsub=${this.has_cnsub}">\n ${year}\n </a>\n `;
let conditionHtml = `\n <div class="button-group">\n <div class="buttons has-addons" id="conditionBox" style="margin-bottom: 0!important;">\n <a style="padding:18px 18px !important;" class="button is-small ${"all" === type ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=all&type_value=&has_cnsub=${this.has_cnsub}">全部</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"0" === type_value ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=0&has_cnsub=${this.has_cnsub}">有码</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"1" === type_value ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=1&has_cnsub=${this.has_cnsub}">无码</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"2" === type_value ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=2&has_cnsub=${this.has_cnsub}">欧美</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"3" === type_value ? "is-info" : ""}" href="/advanced_search?handleTop=1&handleType=video_type&type_value=3&has_cnsub=${this.has_cnsub}">Fc2</a>\n \n <a style="padding:18px 18px !important;margin-left: 50px" class="button is-small ${"1" === this.has_cnsub ? "is-info" : ""}" data-cnsub-value="1">含中字磁鏈</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"0" === this.has_cnsub ? "is-info" : ""}" data-cnsub-value="0">无字幕</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-cnsub-value="">重置</a>\n </div>\n \n <div class="buttons has-addons" id="conditionBox">\n ${yearHtml}\n </div>\n </div>\n `;
for (let year = (new Date).getFullYear(); year >= 2008; year--) yearHtml += `\n <a style="padding:18px 18px !important;" \n class="button is-small ${type_value === year.toString() ? "is-info" : ""}" \n href="/rankings/top?t=y${year}&has_cnsub=${this.has_cnsub}">\n ${year}\n </a>\n `;
let conditionHtml = `\n <div class="button-group">\n <div class="buttons has-addons" id="conditionBox" style="margin-bottom: 0!important;">\n <a style="padding:18px 18px !important;" class="button is-small ${"all" === type ? "is-info" : ""}" href="/rankings/top?has_cnsub=${this.has_cnsub}">全部</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"0" === type_value ? "is-info" : ""}" href="/rankings/top?t=0&has_cnsub=${this.has_cnsub}">有码</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"1" === type_value ? "is-info" : ""}" href="/rankings/top?t=1&has_cnsub=${this.has_cnsub}">无码</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"2" === type_value ? "is-info" : ""}" href="/rankings/top?t=2&has_cnsub=${this.has_cnsub}">欧美</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"3" === type_value ? "is-info" : ""}" href="/rankings/top?t=3&has_cnsub=${this.has_cnsub}">Fc2</a>\n \n <a style="padding:18px 18px !important;margin-left: 50px" class="button is-small ${"1" === this.has_cnsub ? "is-info" : ""}" data-cnsub-value="1">含中字磁鏈</a>\n <a style="padding:18px 18px !important;" class="button is-small ${"0" === this.has_cnsub ? "is-info" : ""}" data-cnsub-value="0">无字幕</a>\n <a style="padding:18px 18px !important;" class="button is-small" data-cnsub-value="">重置</a>\n </div>\n \n <div class="buttons has-addons" id="conditionBox">\n ${yearHtml}\n </div>\n </div>\n `;
this.$contentBox.append(conditionHtml);
$("a[data-cnsub-value]").on("click", (event => {
const cnsubValue = $(event.currentTarget).data("cnsub-value");
@@ -134,7 +146,8 @@ class TOP250Plugin extends BasePlugin {
type = "video_type";
type_value = t;
}
let url = `/advanced_search?handleTop=1&handleType=${type}&type_value=${type_value}`;
let tParam = "year" === type ? `y${type_value}` : "all" === type ? "" : type_value;
let url = `/rankings/top${tParam ? `?t=${tParam}` : ""}`;
event && (event.ctrlKey || event.metaKey) ? GM_openInTab(window.location.origin + url, {
insert: 0
}) : window.location.href = url;
@@ -167,7 +180,7 @@ class TOP250Plugin extends BasePlugin {
await localStorage.setItem("jhs_appAuthorization", token);
show.ok("登录成功");
layer.close(index);
window.location.href = "/advanced_search?handleTop=1&period=daily";
window.location.href = "/rankings/top";
}
}
})).catch((err => {
+200 -9
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);
};
@@ -17,30 +18,49 @@ class WangPan115TaskPlugin extends BasePlugin {
}
async handle() {
$(".buttons button[data-clipboard-text*='magnet:']").each(((i, el) => {
$(el).parent().append($("<button>").text("115离线下载").addClass("button is-info is-small").click((async event => {
const $copyBtn = $(el);
const magnet = $copyBtn.attr("data-clipboard-text");
$copyBtn.css("margin-right", (parseInt($copyBtn.css("margin-right")) - 4) + "px");
const $downloadBtn = $copyBtn.siblings("a.button, button").first();
if ($downloadBtn.length) {
$downloadBtn.css("margin-right", (parseInt($downloadBtn.css("margin-right")) + 3) + "px");
}
$("<button>").text("验车").addClass("button is-small").attr("style", "background-color:#2b6cb0 !important;color:#fff !important;border:none !important;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click((event => {
event.stopPropagation();
event.preventDefault();
this.getBean("MagnetHubPlugin").checkWhatslink(magnet);
})).appendTo($copyBtn.parent());
$("<button>").text("115离线下载").addClass("button is-small").attr("style", "background-color:#4a90e2 !important;color:#fff !important;border:none !important;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click((async event => {
event.stopPropagation();
event.preventDefault();
let loadObj = loading();
try {
await this.handleAddTask($(el).attr("data-clipboard-text"));
await this.handleAddTask(magnet);
} catch (e) {
show.error("发生错误:" + e);
console.error(e);
} finally {
loadObj.close();
}
})));
})).appendTo($copyBtn.parent());
}));
isJavBus && window.isDetailPage && utils.loopDetector((() => $("#magnet-table td a").length > 0), (() => {
this.bus115Down();
}));
}
async bus115Down() {
const $headerRow = $("#magnet-table tr").first();
$("#magnet-table tr").each(((i, row) => {
if (row === $headerRow[0]) return;
const magnetLink = $(row).find("td:nth-child(1) a").attr("href");
if (magnetLink && magnetLink.includes("magnet:")) {
const actionCell = $("<td>").addClass("action-cell");
$("<button>").text("115离线下载").addClass("button is-info is-small").click((async event => {
$("<button>").text("验车").addClass("button is-small").attr("style", "background-color:#2b6cb0 !important;color:#fff !important;border:none !important;margin-right:4px;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click((event => {
event.stopPropagation();
event.preventDefault();
this.getBean("MagnetHubPlugin").checkWhatslink(magnetLink);
})).appendTo(actionCell);
$("<button>").text("115离线下载").addClass("button is-small").attr("style", "background-color:#4a90e2 !important;color:#fff !important;border:none !important;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click((async event => {
event.stopPropagation();
event.preventDefault();
let loadObj = loading();
@@ -56,9 +76,109 @@ class WangPan115TaskPlugin extends BasePlugin {
$(row).append(actionCell);
}
}));
$("#magnet-table tbody").length > 0 && $("#magnet-table tbody tr").append($("<td>").text("操作"));
if ($headerRow.find("td, th").last().text().trim() !== "操作") {
$headerRow.append($("<td>").text("操作"));
}
}
static sanitizeDirName(str) {
if (!str) return "";
return str.replace(/[\\/:*?"<>|]/g, "").replace(/\s+/g, " ").trim();
}
static parseOfflineDirTemplate(str, params) {
if (!str || !params) return str;
return str.replace(/\$\{(\w+)\}/g, (match, key) => {
if (!params.hasOwnProperty(key)) return match;
const value = params[key] || "";
return WangPan115TaskPlugin.sanitizeDirName(String(value));
});
}
static async ensureDir(dirName, pid = 0) {
const result = await gmHttp.postFileFormData("https://webapi.115.com/files/add", {
pid: pid,
cname: dirName
});
if (result && result.file_id) return result.file_id;
const searchRes = await searchFiles(dirName, 0, 1, {fc: 1});
if (searchRes && searchRes.data && searchRes.data.length > 0) {
const match = searchRes.data.find(d => d.pid === pid || d.pid === String(pid));
if (match) return match.fid || match.file_id;
}
return null;
}
static async registerDirAsDownPath(fileId, dirName) {
await gmHttp.postFileFormData("https://webapi.115.com/offine/downpath", {
file_id: fileId
});
const list = await getDownPathList();
const created = list ? list.find(d => d.file_id === fileId || String(d.file_id) === String(fileId)) : null;
return created ? created.id : null;
}
static async createMultiLevelDownPath(templatePath) {
const segments = templatePath.split("/").filter(s => s.trim());
if (segments.length === 0) return null;
let pid = 0;
let fileId = null;
for (const seg of segments) {
fileId = await WangPan115TaskPlugin.ensureDir(seg.trim(), pid);
if (!fileId) {
console.error("[115离线] 创建目录失败:", seg);
return null;
}
pid = fileId;
}
const lastDirName = segments[segments.length - 1].trim();
const downPathId = await WangPan115TaskPlugin.registerDirAsDownPath(fileId, lastDirName);
console.log("[115离线] 多级目录创建完成:", templatePath, "→ downPathId=" + downPathId);
return downPathId;
}
getPageInfoForOffline() {
const baseInfo = this.getPageInfo();
const carNum = baseInfo.carNum || "";
let title = "", maker = "", actor = "";
if (isJavBus) {
title = $("h3").first().text().trim();
maker = $('span.header:contains("製作商:")').parent("p").text().replace("製作商:", "").trim() ||
$('span.header:contains("制作商:")').parent("p").text().replace("制作商:", "").trim();
const actorEl = $('span.genre a[href*="/star/"]').first();
actor = actorEl.length ? actorEl.text().trim() : "";
} else {
title = $(".current-title").first().text().trim() || $("h2.title.is-4 .current-title").first().text().trim() || $(".movie-title").first().text().trim();
maker = $('strong:contains("片商:")').parent().find(".value").text().trim() ||
$('strong:contains("片商:")').parent().find(".value").text().trim();
actor = $('.actor-female').first().text().trim() || $('strong:contains("演員:"), strong:contains("演员:")').parent().find('.value a').first().text().trim() || "";
}
const now = new Date();
if (carNum && title) {
const upperTitle = title.toUpperCase();
const upperCode = carNum.toUpperCase();
if (upperTitle.startsWith(upperCode)) {
title = title.substring(carNum.length).replace(/^[\s\-_]+/, "").trim();
}
}
console.log("[115离线] 页面信息提取:", {code: carNum, title, maker, actor});
return {
code: carNum,
title: title,
maker: maker,
actor: actor,
date: utils.formatDate(now).split(" ")[0]
};
}
async getSavePathId(nyName) {
const enableCustom = await storageManager.getSetting("enable115OfflineFormat", NO);
if (enableCustom === YES) {
const template = await storageManager.getSetting("offlineFolderFormat", "");
if (template && template.includes("${")) {
const pageInfo = this.getPageInfoForOffline();
return WangPan115TaskPlugin.parseOfflineDirTemplate(template, pageInfo);
}
}
let savePath115 = await storageManager.getSetting("savePath115", "云下载");
nyName && (savePath115 = savePath115.replaceAll("{ny}", nyName));
savePath115 = savePath115.replaceAll("{date}", utils.formatDate(new Date));
@@ -79,7 +199,35 @@ class WangPan115TaskPlugin extends BasePlugin {
});
return;
}
const sign = singInfo.sign, time = singInfo.time, userId = this.getUserId(), result = await (async (magnet, wp_path_id = "", uid, sign, time) => {
const sign = singInfo.sign, time = singInfo.time, userId = this.getUserId();
let wp_path_id = savePath || "";
if (!wp_path_id) {
const fullPath = await this.getSavePathId();
console.log("[115离线] 目录模板解析结果:", fullPath);
if (fullPath) {
if (fullPath.includes("/")) {
console.log("[115离线] 检测到多级目录,逐级创建:", fullPath);
const newId = await WangPan115TaskPlugin.createMultiLevelDownPath(fullPath);
if (newId) wp_path_id = newId;
} else {
const downPathList = await getDownPathList();
if (downPathList && downPathList.length > 0) {
const matched = downPathList.find((d => d.name === fullPath));
if (matched) {
wp_path_id = matched.id;
console.log("[115离线] 目录匹配结果: 匹配到 \"" + matched.name + "\" -> id=" + matched.id);
} else {
console.log("[115离线] 目录匹配结果: 未匹配到 \"" + fullPath + "\",自动创建并注册");
const newId = await WangPan115TaskPlugin.createMultiLevelDownPath(fullPath);
if (newId) wp_path_id = newId;
}
}
}
}
}
const result = await (async (magnet, wp_path_id = "", uid, sign, time) => {
const data = {
url: encodeURIComponent(magnet),
wp_path_id: wp_path_id,
@@ -88,7 +236,7 @@ class WangPan115TaskPlugin extends BasePlugin {
time: time
};
return await gmHttp.postForm("https://115.com/web/lixian/?ct=lixian&ac=add_task_url", data);
})(magnetLink, savePath || "", userId, sign, time);
})(magnetLink, wp_path_id, userId, sign, time);
console.log("离线下载返回值:", result);
let infoHash = result.info_hash, fileId = await this.getFileId(userId, sign, time, infoHash), openUrl = "https://115.com/?tab=offline&mode=wangpan";
fileId && (openUrl = `https://115.com/?cid=${fileId}&offset=0&mode=wangpan`);
@@ -145,6 +293,49 @@ class WangPan115TaskPlugin extends BasePlugin {
}
return fileId;
}
//TODO: 暂时没有删除离线任务
// async deleteOfflineTasksByKeyword(keyword) {
// try {
// const singInfo = await (async () => {
// const res = await gmHttp.get("https://115.com/?ct=offline&ac=space&_=" + (new Date).getTime());
// return "object" == typeof res ? res : null;
// })();
// if (!singInfo) {
// console.error("删除离线任务: 未登录115网盘");
// return;
// }
// const {sign: sign, time: time} = singInfo, uid = singInfo.uid || (() => {
// const match = document.cookie.match(/UID=(\d+)_/);
// return match ? match[1] : null;
// })() || await this.getUserId();
// const taskListRes = await gmHttp.postForm("https://115.com/web/lixian/?ct=lixian&ac=task_lists", {
// page: 1,
// uid: uid,
// sign: sign,
// time: time
// });
// const tasks = (null == taskListRes ? void 0 : taskListRes.tasks) || [];
// const keyword2 = keyword.toLowerCase().replace("fc2-", "");
// let deletedCount = 0;
// for (const task of tasks) {
// if (task.name && task.name.toLowerCase().includes(keyword2)) {
// console.log(`删除离线任务: ${task.name} (hash: ${task.info_hash})`);
// await gmHttp.postForm("https://115.com/web/lixian/?ct=lixian&ac=task_del", {
// "hash[0]": task.info_hash,
// flag: 1,
// uid: uid,
// sign: sign,
// time: time
// });
// deletedCount++;
// }
// }
// deletedCount > 0 && console.log(`共删除 ${deletedCount} 个离线任务`);
// } catch (error) {
// console.error("删除离线任务失败:", error);
// }
// }
}
+376 -14
View File
@@ -1,6 +1,7 @@
import { BasePlugin } from '../core/base-plugin.js';
import { __publicField, isJavBus, isJavDb, currentHref, NO } from '../core/constants.js';
import { searchFiles } from './wangpan-115-task.js';
import {JavPackSubtitle} from './subtitles.js';
class WangPan115Plugin extends BasePlugin {
constructor() {
@@ -240,6 +241,19 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
event.stopPropagation();
await this.retryMatch(event.currentTarget);
}));
$(document).on("click", ".jhs-detail-delete-btn", (async event => {
event.preventDefault();
event.stopPropagation();
const $btn = $(event.currentTarget);
const matchData = $btn.attr("data-match");
if (!matchData) return;
const matchList = JSON.parse(matchData);
const carNum2 = this.getPageInfo().carNum;
this.showDeleteDialog(carNum2, matchList, (async newMatchList => {
$("#115-match-table").remove();
await this.matchDetailPage(newMatchList);
}));
}));
await this.matchDetailPage();
$(document).on("click", ".jhs-match-detail-error-btn", (async event => {
event.preventDefault();
@@ -261,11 +275,19 @@ 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("");
$tbody.append(rowsHtml);
$detail.css("position", "relative");
$detail.append(`<span class="jhs-detail-delete-btn" \n data-match='${JSON.stringify(matchList)}'\n style="position: absolute; top: 8px; right: 12px; cursor: pointer; color: #e74c3c; font-size: 16px; font-weight: bold; z-index: 1;" \n title="删除115作品">🗑️ 删除</span>`);
} else $tbody.append(`<tr><td colspan="4">\n <a class='jhs-match-detail-error-btn a-info'\n data-keyword="${carNum2}"\n title="未匹配,点击重试">未匹配</a>\n </td></tr>`);
} catch (error) {
$tbody.append(`<tr><td colspan="4">\n <a class="a-danger jhs-match-detail-error-btn" title="${error.message || "加载失败"}">加载失败,请重试</a>\n </td></tr>`);
@@ -341,25 +363,335 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
}
}
updateMatchStatus($box2, carNum2, matchList) {
matchList.length > 0 ? $box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-btn a-success' \n data-keyword="${carNum2}"\n data-match='${JSON.stringify(matchList)}'\n title="点击查看匹配详情">匹配${matchList.length}个</a>`) : $box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn a-info' data-keyword="${carNum2}" \n title="点击重新尝试匹配">未匹配</a>`);
if (matchList.length > 0) {
$box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-btn a-success' \n data-keyword="${carNum2}"\n data-match='${JSON.stringify(matchList)}'\n title="点击查看匹配详情">匹配${matchList.length}个</a>`);
const $deleteBtn = $box2.find(".delete115Svg");
const $markBtn = $box2.find(".markDeleteSvg");
if ($deleteBtn.length > 0 && matchList.some(m => m.dirId != null)) {
$deleteBtn.attr("data-match", JSON.stringify(matchList));
$deleteBtn.show();
}
if ($markBtn.length > 0) {
$markBtn.show();
this.updateMarkBtnStyle($markBtn, carNum2);
}
} else {
$box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn a-info' data-keyword="${carNum2}" \n title="点击重新尝试匹配">未匹配</a>`);
const $deleteBtn = $box2.find(".delete115Svg");
const $markBtn = $box2.find(".markDeleteSvg");
if ($deleteBtn.length > 0) {
$deleteBtn.removeAttr("data-match");
$deleteBtn.hide();
}
if ($markBtn.length > 0) {
$markBtn.hide();
}
}
}
async updateMarkBtnStyle($markBtn, carNum2) {
const markedList = await storageManager.forage.getItem("markedDeleteList") || [];
const isMarked = markedList.some(item => item.carNum === carNum2);
isMarked ? $markBtn.addClass("marked") : $markBtn.removeClass("marked");
$markBtn.attr("title", isMarked ? "取消标记删除" : "标记删除");
}
async batchDeleteMarked(selectedItems, onProgress) {
let deletedCount = 0;
let failedCount = 0;
const 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 {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 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++;
}
} 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}个` : "删除失败");
} catch (error) {
failedCount++;
onProgress && onProgress(i + 1, selectedItems.length, item, error.message || "出错");
console.error(`批量删除出错 [${item.carNum}]:`, error);
}
}
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 {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中删除。`);
return;
}
const formatSize = bytes => {
if (!bytes) return "-";
const units = ["B", "KB", "MB", "GB", "TB"];
let size = parseFloat(bytes), unit = 0;
for (; size >= 1024 && unit < units.length - 1;) size /= 1024, unit++;
return size.toFixed(2) + " " + units[unit];
};
let html = '<div style="padding: 15px; max-height: 400px; overflow-y: auto;">';
html += `<div style="margin-bottom: 12px; font-size: 14px;">作品 <b>${carNum2}</b> 匹配到 <b>${matchList.length}</b> 个文件夹(可删除 <b>${safeMatchList.length}</b> 个):</div>`;
if (unsafeCount > 0) {
html += `<div style="margin-bottom: 10px; padding: 6px 10px; background: #fff3cd; border: 1px solid #ffc107; border-radius: 4px; font-size: 12px; color: #856404;">⚠ 已自动排除 ${unsafeCount} 个目录名不含番号的项目,防止误删</div>`;
}
html += '<div style="margin-bottom: 10px;"><label style="cursor: pointer;"><input type="checkbox" class="jhs-delete-select-all" checked> <b>全选</b></label></div>';
for (let i = 0; i < safeMatchList.length; i++) {
const m = safeMatchList[i];
html += `<div style="margin-bottom: 6px; padding: 8px 10px; border: 1px solid #e0e0e0; border-radius: 4px; background: #fafafa;">
<label style="display: flex; align-items: flex-start; cursor: pointer; width: 100%;">
<input type="checkbox" class="jhs-delete-folder-cb" data-index="${i}" checked style="margin-top: 3px; flex-shrink: 0;">
<span style="margin-left: 8px; flex: 1;">
<div style="word-break: break-all;">${m.name}</div>
<div style="font-size: 12px; color: #888; margin-top: 2px;">${formatSize(m.size)} | ${m.createTime}</div>
</span>
</label>
</div>`;
}
html += "</div>";
const layerIndex = layer.open({
type: 1,
title: "删除115作品文件夹",
content: html,
area: ["520px", "auto"],
btn: ["确定删除", "取消"],
shadeClose: !1,
yes: async index => {
const $layer = $(`#layui-layer${index}`);
const checked = $layer.find(".jhs-delete-folder-cb:checked");
if (0 === checked.length) {
show.error("请至少选择一个文件夹");
return;
}
const selected = checked.map((function () {
return parseInt($(this).data("index"));
})).get();
layer.close(index);
let loadObj = loading();
let deletedCount = 0;
try {
const 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} 个文件夹`);
const newMatchList = await this.searchFiles(carNum2);
afterDelete && afterDelete(newMatchList);
const remainingCids = new Set(newMatchList.map((m) => m.dirId));
JavPackSubtitle.cached115Matches = JavPackSubtitle.cached115Matches.filter((m) => remainingCids.has(m.cid));
JavPackSubtitle.current115Cids = JavPackSubtitle.current115Cids.filter((cid) => remainingCids.has(cid));
try {
this.getBean("WangPan115TaskPlugin").deleteOfflineTasksByKeyword?.(carNum2);
} catch (e) {
console.warn("删除离线任务暂不可用:", e.message);
}
} catch (error) {
loadObj.close();
console.error("删除115文件失败:", error);
show.error(`删除失败: ${error.message || "网络错误"}`);
}
}
});
$(`#layui-layer${layerIndex}`).on("change", ".jhs-delete-select-all", (function () {
const checked = $(this).prop("checked");
$(`#layui-layer${layerIndex} .jhs-delete-folder-cb`).prop("checked", checked);
}));
}
async handleLoginRedirect() {
window.open("https://115.com");
}
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,
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>`);
@@ -392,6 +724,16 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
if (!($box2.find("[class^='jhs-match-']").length > 0)) if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $box2.find(".video-title").prepend(`<a class='jhs-match-no-login-btn a-info' \n data-keyword="${carNum2}" \n title="未登录115网盘">未登录</a>`); else if (matchList.length > 0) {
const title = 1 === matchList.length ? "点击直接播放" : `点击查看${matchList.length}个匹配结果`;
$box2.find(".video-title").prepend(`<a class='jhs-match-btn a-success' \n data-keyword="${carNum2}"\n data-match='${JSON.stringify(matchList)}'\n title="${title}">匹配${matchList.length}个</a>`);
const $deleteBtn = $box2.find(".delete115Svg");
const $markBtn = $box2.find(".markDeleteSvg");
if ($deleteBtn.length > 0 && matchList.some(m => m.dirId != null)) {
$deleteBtn.attr("data-match", JSON.stringify(matchList));
$deleteBtn.show();
}
if ($markBtn.length > 0) {
$markBtn.show();
this.updateMarkBtnStyle($markBtn, carNum2);
}
} else $box2.find(".video-title").prepend(`<a class='jhs-match-error-btn a-info' \n data-keyword="${carNum2}" \n title="未匹配,点击重试">未匹配</a>`);
}
formatSize(bytes) {
@@ -414,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 };
+6 -10
View File
@@ -6,11 +6,11 @@ export default defineConfig({
monkey({
entry: 'src/main.js',
userscript: {
name: 'JAV-JHS',
namespace: 'JAV-JHS',
version: '3.3.6-fix',
author: 'xie bro,fireinrain',
description: 'Jav-鉴黄师 收藏、屏蔽、标记已下载; 屏蔽标签、屏蔽演员、同步收藏演员、新作品检测; 免VIP查看热播、Top250排行榜、Fc2ppv、可查看所有评论信息、相关清单; 支持云盘备份; 以图识图; 字幕搜索; JavDb|JavBus',
name: 'JHS-enhance',
namespace: 'JHS-enhance',
version: '3.3.18',
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',
match: [
@@ -37,9 +37,7 @@ export default defineConfig({
'https://*seejav*/*actresses',
],
require: [
'https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js',
'https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js',
'https://cdn.jsdelivr.net/npm/tabulator-tables@6.3.1/dist/js/tabulator.min.js',
'https://cdn.jsdelivr.net/npm/layui-layer@1.0.9/dist/layer.min.js',
'https://cdn.jsdelivr.net/npm/blueimp-md5@2.19.0/js/md5.min.js',
'https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js',
@@ -93,14 +91,12 @@ 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',
},
},
}),