Compare commits

...

54 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
39 changed files with 46018 additions and 12169 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
+153 -73
View File
@@ -2,7 +2,7 @@
> Jav-鉴黄师 油猴脚本 — 工程化重构版本
基于 `vite-plugin-monkey` 脚手架,将原 10,000+ 行单体脚本拆分为 49 个可维护的 ES Module 模块,支持本地开发热更新、一键构建。
基于 `vite-plugin-monkey` 脚手架,将原 10,000+ 行单体脚本拆分为 56 个可维护的 ES Module 模块,支持本地开发热更新、一键构建。
---
@@ -23,31 +23,69 @@
---
## 自动部署
本项目使用 GitHub Actions 在推送代码时自动构建并发布到 [GitHub Releases](https://github.com/fireinrain/JHS-enhance/releases)。
```
git push → GitHub Actions 自动构建 → 发布 GitHub Release
↓
https://github.com/fireinrain/JHS-enhance/releases/latest/download/jhs-enhance.user.js
↓
用户点击安装链接 → Tampermonkey 弹出安装提示
```
---
## 功能概览
- **列表页增强**:作品状态标签(屏蔽/收藏/已下载/已观看)、一键筛选已收藏/新作品、演员黑名单过滤、标题关键词过滤
- **详情页增强**:磁力链接高亮匹配、DMM 多画质预览视频、标题翻译(Google 翻译)、字幕搜索(整合迅雷 + SubTitleCat,支持 115
网盘直传与多目录选择)、演员信息展示、相关清单、评论查看
- **多源预览图**:javfree / projectjav / javstore 三源预览图,支持来源切换、下载、新窗口打开,带 sessionStorage 缓存
- **115 网盘集成**:目录匹配与预加载、多目录选择上传、字幕一键直传
- **数据管理**:IndexedDB 本地存储、云盘备份/恢复(阿里云盘 / 115 网盘 / WebDAV)、跨 Tab 数据同步
- **自动化任务**:自动翻页、新作品检测、收藏演员同步、黑名单更新
- **UI 优化**:分类折叠、热榜 Top250、免 VIP 查看热播、以图识图、FC2 支持
- **多站点支持**:JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers / SubTitleCat
### 列表页增强
- 作品状态标签(屏蔽/收藏/已下载/已观看)
- 一键筛选已收藏/新作品/待鉴定
- 演员黑名单过滤、标题关键词过滤
- 封面快捷按钮(长缩略图/预览视频/鉴定按钮/第三方跳转/复制番号)
- 分类折叠、热榜 Top250、免 VIP 查看热播
- 自动翻页
### 详情页增强
- **磁力搜索聚合(MagnetHub)**:多引擎聚合搜索(Sukebei / U9A9 / U3C3 / BTSearch / SoKitty / ØMagnet),Tab
切换引擎,结果表格展示(名称/大小/日期/操作),支持按大小/最新/最旧排序,质量标签自动识别(中字/4K/破解),一键复制磁力链接(含番号提取码),验车功能(Whatslink
API 截图预览、键盘/缩放导航),115 离线下载快捷按钮
- **磁力排序与过滤(JavDB)**:按发布日期/文件大小/文件数量拖拽排序,勾选启用;高清/4K/字幕/无码多条件并集过滤,4K 磁力行高亮显示
- 磁力链接高亮匹配已收藏/已下载状态
- DMM 多画质预览视频
- **多源视频预览**: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
---
@@ -59,63 +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/ # 插件层(40+ 个功能插件)
│ ├── list-page.js # 列表页过滤与状态标签
│ ├── list-page-button.js # 列表页功能按钮(待鉴定/已收藏/新作品/黑名单)
│ ├── detail-page.js # 详情页数据加载
│ ├── detail-page-button.js# 详情页操作按钮(含 115 目录预加载)
│ ├── setting.js # 设置面板(Tabulator 表格)
│ ├── translate.js # 标题翻译(Google 翻译 API)
│ ├── preview-video.js # DMM 预览视频
│ ├── highlight-magnet.js # 磁力链接高亮匹配
│ ├── auto-page.js # 自动翻页 / 云盘备份
│ ├── blacklist.js # 黑名单管理(演员/番号)
│ ├── favorite-actresses.js# 收藏演员同步
│ ├── new-video.js # 新作品检测
│ ├── task.js # 后台任务调度
│ ├── history.js # 浏览历史
│ ├── wangpan-115.js # 115 网盘匹配
│ ├── wangpan-115-task.js # 115 网盘任务处理
│ ├── aliyun-pan.js # 阿里云盘
│ ├── main.js # 入口文件:全局初始化、插件注册、页面启动、loading/Toast/Viewer 全局函数
│ ├── core/ # 基础设施层(7 个模块)
│ │ ├── constants.js # 全局常量、CSS 样式、页面类型判断、枚举值
│ │ ├── base-plugin.js # 插件基类(含 SVG 图标、页面信息解析、getBean 跨插件调用)
│ │ ├── plugin-manager.js # 插件管理器(注册、CSS 注入、插件生命周期、按站点分组)
│ │ ├── globals.js # 全局依赖挂载(jQuery、localforage、layer 等)
│ │ ├── storage.js # IndexedDB 存储管理器(localforage 封装,含缓存)
│ │ ├── utils.js # 工具函数集(DOM 操作、弹窗确认、快捷键、重试、日期格式化)
│ │ └── gmHttp.js # GM_xmlhttpRequest 封装(GET/POST/Form/File、超时重试、分块下载)
│ ├── api/ # API 层(4 个模块)
│ │ ├── javdb.js # JavDB API 封装(签名算法、搜索、评论、磁力列表)
│ │ ├── dmm.js # DMM 预览视频获取(多画质、默认画质选择)
│ │ ├── dmm-graphql.js # DMM GraphQL API(备选视频源回退)
│ │ └── 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 # 多源预览图(javfree/projectjav/javstore)
│ ├── subtitles.js # 字幕搜索(整合迅雷+SubTitleCat)+ 115 直传
│ ├── req115.js # 115 网盘 API 封装(Req + Drive115 + Req115)
│ ├── magnet-hub.js # MagnetHub 磁力搜索
│ ├── other-site.js # 第三方站点链接
│ ├── jav-trailers.js # JavTrailers 站点
│ ├── subtitle-cat.js # SubTitleCat 字幕搜索(已整合到 subtitles.js,保留文件)
│ ├── image-recognition.js # 以图识图(第三方识图接口)
│ ├── screenshot.js # 多源预览图(javfree/projectjav/javstore + 来源切换弹窗)
│ ├── subtitles.js # 字幕搜索(整合迅雷+SubTitleCat)+ 115 多目录直传
│ ├── req115.js # 115 网盘 API 封装(Req 基类 + Drive115 + Req115)
│ ├── magnet-hub.js # MagnetHub 磁力搜索(6 引擎聚合、质量标签、验车截图、115 离线)
│ ├── other-site.js # 第三方站点跳转链接
│ ├── jav-trailers.js # JavTrailers 站点处理
│ ├── javxy-preview-video.js# 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 # 原始单体脚本(参考归档)
```
---
@@ -245,6 +289,12 @@ window.show.ok('操作成功'); // Toast 通知
---
## 赞助
如果当前项目有帮到您的话,赞助我喝瓶水/Star项目也是极好的,感谢支持 :)
![赞助](https://i.imgur.com/ynGK6he.jpeg)
## 构建配置
`vite-plugin-monkey` 在 `vite.config.mjs` 中配置了:
@@ -258,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(基础设施层)** 开始,这是最独立、最不容易出错的模块。
+39920 -11432
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,
};
+1 -4
View File
@@ -145,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,
+230
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",
@@ -569,6 +579,215 @@ const mainCss = `
background: rgba(59, 130, 246, 0.9);
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4);
}
.magnet-4k-highlight {
background-color: #fff5f5 !important;
}
.magnet-4k-highlight .name {
color: #f40 !important;
}
#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>
`;
@@ -851,6 +1070,16 @@ 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,
__typeError,
@@ -860,6 +1089,7 @@ export {
isJavDb,
isJavBus,
isSearchPage,
isEligibleDmmCoverCode,
Status_RUNNING,
Status_SUCCESS,
Status_FAIL,
+9 -3
View File
@@ -7,6 +7,14 @@ import QRCode from 'qrcodejs';
import { currentHref, isJavDb, isJavBus, isSearchPage, Status_RUNNING, Status_SUCCESS, Status_FAIL, Status_LOADING, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH, NO, YES } from './constants.js';
window.$ = window.jQuery = $;
if (typeof $.trim !== 'function') {
$.trim = function (text) {
return text == null ? '' : (text + '').trim();
};
}
if (typeof jQuery !== 'undefined' && typeof layer === 'undefined') {
await import('layui-layer');
}
window.localforage = localforage;
window.Toastify = Toastify;
window.md5 = md5;
@@ -26,6 +34,4 @@ window.Status_FAVORITE = Status_FAVORITE;
window.Status_HAS_DOWN = Status_HAS_DOWN;
window.Status_HAS_WATCH = Status_HAS_WATCH;
window.NO = NO;
window.YES = YES;
import 'layui-layer';
window.YES = YES;
+1 -1
View File
@@ -98,7 +98,7 @@ class Utils {
}
if (!viewerExists) {
this.layerIndexStack.pop();
layer.close(topLayerIndex);
typeof layer !== 'undefined' && layer.close(topLayerIndex);
}
}
setupEscClose(layerIndex) {
+266
View File
@@ -0,0 +1,266 @@
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
const HLS_SCRIPT_URL = 'https://cdn.jsdelivr.net/npm/hls.js@1.5.18/dist/hls.min.js';
let hlsLoadPromise = null;
const getHlsClass = () => {
const HlsClass = window.Hls || globalThis.Hls || (typeof Hls !== 'undefined' ? Hls : null);
return HlsClass && HlsClass.isSupported && HlsClass.isSupported() ? HlsClass : null;
};
const binaryTextToArrayBuffer = (value) => {
const text = String(value || '');
const bytes = new Uint8Array(text.length);
for (let index = 0; index < text.length; index += 1) {
bytes[index] = text.charCodeAt(index) & 0xff;
}
return bytes.buffer;
};
const loadHlsLibrary = () => {
const readyHls = getHlsClass();
if (readyHls) return Promise.resolve(readyHls);
if (hlsLoadPromise) return hlsLoadPromise;
const loadByGm = () => new Promise((resolve) => {
GM_xmlhttpRequest({
method: 'GET',
url: HLS_SCRIPT_URL,
timeout: 15000,
onload: (r) => {
if (r.status >= 200 && r.status < 300 && r.responseText) {
try {
Function(`${r.responseText}\n//# sourceURL=${HLS_SCRIPT_URL}`).call(globalThis);
} catch (err) {
clog.error('HLS: hls.js 执行失败', err);
}
}
resolve(getHlsClass());
},
onerror: () => resolve(getHlsClass()),
ontimeout: () => resolve(getHlsClass()),
});
});
hlsLoadPromise = new Promise((resolve) => {
const existing = document.querySelector('script[data-jhs-hls="1"]');
if (existing) {
existing.addEventListener('load', () => resolve(getHlsClass()), {once: true});
existing.addEventListener('error', () => loadByGm().then(resolve), {once: true});
setTimeout(() => {
if (!getHlsClass()) loadByGm().then(resolve);
}, 4000);
return;
}
const hlsScript = document.createElement('script');
hlsScript.src = HLS_SCRIPT_URL;
hlsScript.async = true;
hlsScript.dataset.jhsHls = '1';
hlsScript.onload = () => resolve(getHlsClass());
hlsScript.onerror = () => loadByGm().then(resolve);
document.head.appendChild(hlsScript);
}).then((HlsClass) => {
if (!HlsClass) hlsLoadPromise = null;
return HlsClass;
});
return hlsLoadPromise;
};
const createHlsLoader = () => {
class GMHlsLoader {
constructor(config) {
this.config = config;
this.context = null;
this.callbacks = null;
this.loader = null;
this.stats = this.createStats();
}
createStats() {
return {
aborted: false,
loaded: 0,
retry: 0,
total: 0,
chunkCount: 0,
bwEstimate: 0,
trequest: 0,
tfirst: 0,
tload: 0,
loading: {start: 0, first: 0, end: 0},
parsing: {start: 0, end: 0},
buffering: {start: 0, first: 0, end: 0},
};
}
destroy() {
this.abort();
}
abort() {
if (this.stats) this.stats.aborted = true;
if (this.loader) {
try {
this.loader.abort();
} catch (e) { /* ignore */
}
}
this.loader = null;
}
load(context, config, callbacks) {
this.context = context;
this.callbacks = callbacks;
const requestUrl = context.url;
const wantsArrayBuffer =
context.responseType === 'arraybuffer' ||
/\.(?:ts|m4s|mp4|key)(?:[?#]|$)/i.test(requestUrl);
const startedAt = performance.now();
const stats = (this.stats = this.createStats());
stats.trequest = startedAt;
stats.tfirst = startedAt;
stats.tload = startedAt;
stats.loading.start = startedAt;
this.loader = GM_xmlhttpRequest({
method: 'GET',
url: requestUrl,
responseType: 'text',
overrideMimeType: wantsArrayBuffer ? 'text/plain; charset=x-user-defined' : undefined,
timeout: config && config.timeout ? config.timeout : 20000,
headers: {
Accept: wantsArrayBuffer
? '*/*'
: 'application/vnd.apple.mpegurl, application/x-mpegURL, */*',
},
onprogress: (event) => {
stats.loaded = Number(event && event.loaded ? event.loaded : stats.loaded || 0);
stats.total = Number(event && event.total ? event.total : stats.total || stats.loaded || 0);
if (!stats.loading.first && stats.loaded > 0) {
stats.loading.first = performance.now();
}
},
onload: (r) => {
const status = Number(r.status || 0);
const response = {
code: status,
text: r.statusText || '',
url: r.finalUrl || requestUrl,
};
stats.tfirst = stats.tfirst || performance.now();
stats.tload = performance.now();
stats.loading.first = stats.loading.first || stats.tload;
stats.loading.end = stats.tload;
if (status < 200 || status >= 300) {
callbacks.onError && callbacks.onError(response, context, null, stats);
return;
}
const responseText = r.responseText || r.response || '';
const data = wantsArrayBuffer
? binaryTextToArrayBuffer(responseText)
: responseText;
stats.loaded = data && data.byteLength ? data.byteLength : (data && data.length ? data.length : stats.loaded || 0);
stats.total = stats.total || stats.loaded;
stats.bwEstimate =
stats.loading.end > stats.loading.first
? Math.round((stats.total * 8000) / (stats.loading.end - stats.loading.first))
: 0;
callbacks.onSuccess && callbacks.onSuccess({data, url: response.url}, stats, context, response);
},
onerror: () => {
callbacks.onError &&
callbacks.onError({code: 0, text: 'network error', url: requestUrl}, context, null, stats);
},
ontimeout: () => {
stats.tload = performance.now();
stats.loading.end = stats.tload;
callbacks.onTimeout && callbacks.onTimeout(stats, context, null);
},
});
}
}
return GMHlsLoader;
};
const isM3U8Url = (url) => /\.m3u8(?:[?#].*)?$/i.test(String(url || ''));
const attachHlsToVideo = (videoEl, src) => {
return new Promise((resolve, reject) => {
if (!videoEl || !src) {
reject(new Error('video element or source missing'));
return;
}
const HlsClass = getHlsClass();
if (!HlsClass) {
videoEl.src = src;
videoEl.load && videoEl.load();
resolve(false);
return;
}
const hls = new HlsClass({
enableWorker: false,
lowLatencyMode: true,
loader: createHlsLoader(),
autoStartLoad: true,
startPosition: 0,
capLevelToPlayerSize: true,
testBandwidth: false,
preferManagedMediaSource: false,
maxBufferLength: 6,
maxMaxBufferLength: 12,
backBufferLength: 30,
maxBufferHole: 0.5,
nudgeOffset: 0.1,
manifestLoadingMaxRetry: 2,
levelLoadingMaxRetry: 2,
fragLoadingMaxRetry: 2,
manifestLoadingTimeOut: 12000,
levelLoadingTimeOut: 12000,
fragLoadingTimeOut: 12000,
abrEwmaFastLive: 3,
abrEwmaSlowLive: 9,
});
hls.on(HlsClass.Events.MANIFEST_PARSED, () => {
hls.startLoad(0);
videoEl.play().catch(() => {
});
resolve(true);
});
hls.on(HlsClass.Events.ERROR, (_, data) => {
if (!data || !data.fatal) return;
clog.error('HLS: 播放失败', data);
if (data.type === HlsClass.ErrorTypes.MEDIA_ERROR) {
if (videoEl.readyState >= 2) {
try {
hls.recoverMediaError && hls.recoverMediaError();
} catch (e) { /* ignore */
}
return;
}
}
reject(new Error(data.details || 'HLS fatal error'));
});
hls.loadSource(src);
hls.attachMedia(videoEl);
videoEl._hls = hls;
});
};
const destroyHls = (videoEl) => {
if (videoEl && videoEl._hls) {
try {
videoEl._hls.destroy();
} catch (e) { /* ignore */
}
videoEl._hls = null;
}
};
export {loadHlsLibrary, getHlsClass, createHlsLoader, isM3U8Url, attachHlsToVideo, destroyHls, HLS_SCRIPT_URL};
+33 -21
View File
@@ -6,6 +6,9 @@ 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;
@@ -29,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';
@@ -51,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)();
@@ -819,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");
@@ -858,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);
@@ -876,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);
@@ -906,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
+283 -11
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() {
@@ -23,7 +36,7 @@ class DetailPageButtonPlugin extends BasePlugin {
}
async createMenuBtn() {
const pageInfo = this.getPageInfo(), carNum2 = pageInfo.carNum,
buttonsHtml = '\n <div style="margin: 10px auto; display: flex; justify-content: space-between; align-items: center; flex-wrap:wrap;gap: 20px;">\n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="filterBtn" class="menu-btn" style="width: 120px; background-color:#de3333; color: white; text-align: center; padding: 8px 0;">\n <span>🚫 屏蔽</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="width: 120px; background-color:#25b1dc; color: white; text-align: center; padding: 8px 0;">\n <span>⭐ 收藏</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="width: 120px; background-color:#7bc73b; color: white; text-align: center; padding: 8px 0;">\n <span>📥️ 已下载</span>\n </a>\n <a id="hasWatchBtn" class="menu-btn" style="width: 120px; background-color:#d7a80c; color: white; text-align: center; padding: 8px 0;">\n <span>🔍 已观看</span>\n </a>\n </div>\n \n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="enable-magnets-filter" class="menu-btn" style="width: 140px; background-color: #c2bd4c; color: white; text-align: center; padding: 8px 0;">\n <span id="magnets-span">关闭磁力过滤</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n <a id="subtitleSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕搜索</span>\n </a>\n <!--\n <a id="xunLeiSubtitleBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="search-subtitle-btn" class="menu-btn" style="width: 160px; background: linear-gradient(to bottom, #8d5656, rgb(196,159,91)); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (SubTitleCat)</span>\n </a>\n -->\n </div>\n </div>\n ';
buttonsHtml = '\n <div style="margin: 10px auto; display: flex; justify-content: space-between; align-items: center; flex-wrap:wrap;gap: 20px;">\n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="filterBtn" class="menu-btn" style="width: 120px; background-color:#de3333; color: white; text-align: center; padding: 8px 0;">\n <span>🚫 屏蔽</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="width: 120px; background-color:#25b1dc; color: white; text-align: center; padding: 8px 0;">\n <span>⭐ 收藏</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="width: 120px; background-color:#7bc73b; color: white; text-align: center; padding: 8px 0;">\n <span>📥️ 已下载</span>\n </a>\n <a id="hasWatchBtn" class="menu-btn" style="width: 120px; background-color:#d7a80c; color: white; text-align: center; padding: 8px 0;">\n <span>🔍 已观看</span>\n </a>\n </div>\n \n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="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()));
@@ -43,29 +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);
}
}));
$("#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)}`);
+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();
+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};
+17 -2
View File
@@ -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",
+35 -7
View File
File diff suppressed because one or more lines are too long
+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 => {
+157 -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`);
+207 -39
View File
@@ -275,7 +275,13 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
const $detail = $('\n <div class="jhs-match-detail jhs-115-match-detail" id="115-match-table">\n <table>\n <thead>\n <tr style="text-align: center">\n <th colspan="4">115匹配</th>\n </tr>\n <tr>\n <th>名称</th>\n <th>大小</th>\n <th>时间</th>\n <th>播放</th>\n </tr>\n </thead>\n <tbody>\n </tbody>\n </table>\n </div>\n '), $tbody = $detail.find("tbody");
try {
const carNum2 = this.getPageInfo().carNum;
matchList || (matchList = await this.searchFiles(carNum2));
if (!matchList) {
const {results, usedPhase2} = await this._searchFilesWithPhase(carNum2);
matchList = results;
if (usedPhase2 && matchList.length > 0) {
await this.autoRenameFiles(carNum2, matchList);
}
}
await this.checkLoginStatus();
if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $tbody.append(`<tr><td colspan="4">\n <a class='jhs-match-no-login-btn a-info'\n data-keyword="${carNum2}"\n title="未登录115网盘">未登录</a>\n </td></tr>`); else if (matchList.length > 0) {
const rowsHtml = matchList.map((match => `\n <tr>\n <td>${match.name}</td>\n <td>${this.formatSize(match.size)}</td>\n <td>${match.createTime}</td>\n <td>\n <a href="https://115vod.com/?pickcode=${match.videoId}&share_id=0"\n target="_blank"\n class="a-success"\n title="播放">播放</a>\n </td>\n </tr>\n `)).join("");
@@ -393,43 +399,56 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
async batchDeleteMarked(selectedItems, onProgress) {
let deletedCount = 0;
let failedCount = 0;
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
const randomDelay = () => new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000));
for (let i = 0; i < selectedItems.length; i++) {
const item = selectedItems[i];
try {
if (i > 0) {
await randomDelay();
}
const matchList = await this.searchFiles(item.carNum);
if (matchList.length === 0) {
failedCount++;
onProgress && onProgress(i + 1, selectedItems.length, item, "未匹配到115文件");
continue;
}
const upperCarNum = item.carNum.toUpperCase();
const safeMatchList = matchList.filter(m => m.name.toUpperCase().includes(upperCarNum));
const {regex: safeRegex} = codeParse(item.carNum);
const safeMatchList = matchList.filter(m => safeRegex.test(m.name));
if (safeMatchList.length === 0) {
failedCount++;
onProgress && onProgress(i + 1, selectedItems.length, item, "无安全可删除目录");
continue;
}
let itemDeleted = 0;
const uniqueDirIds = [...new Set(safeMatchList.map((m) => m.dirId))];
for (const dirId of uniqueDirIds) {
try {
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
"fid[0]": dirId,
ignore_warn: "1"
});
if (result && result.state) {
const groupedByDir = {};
for (const m of safeMatchList) {
if (!groupedByDir[m.dirId]) groupedByDir[m.dirId] = [];
groupedByDir[m.dirId].push(m);
}
const entries = Object.entries(groupedByDir);
for (let g = 0; g < entries.length; g++) {
if (g > 0) {
await randomDelay();
}
const [dirId, items] = entries[g];
if (items.length === 1) {
const result = await this._deleteOne(dirId, false, i, selectedItems, item, onProgress);
if (result) {
itemDeleted++;
deletedCount++;
}
} catch (e) {
console.error(`删除目录失败: ${dirId}`, e);
} else {
for (let f = 0; f < items.length; f++) {
if (f > 0) await randomDelay();
const fileDeleted = await this._deleteOne(items[f].folderId, true, i, selectedItems, item, onProgress);
if (fileDeleted) {
itemDeleted++;
deletedCount++;
}
}
}
}
onProgress && onProgress(i + 1, selectedItems.length, item, itemDeleted > 0 ? `已删除${itemDeleted}个` : "删除失败");
if (i < selectedItems.length - 1) {
await delay(800);
}
} catch (error) {
failedCount++;
onProgress && onProgress(i + 1, selectedItems.length, item, error.message || "出错");
@@ -439,9 +458,63 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
return {deletedCount, failedCount};
}
isRateLimited(response) {
if (!response) return false;
if (response.state === false) {
const msg = (response.error || response.msg || "").toLowerCase();
if (msg.includes("频繁") || msg.includes("操作太频繁") || msg.includes("稍后再试")
|| msg.includes("rate") || msg.includes("limit") || msg.includes("too many")) {
return true;
}
}
if (response.status === 403 || response.status === 429) {
return true;
}
return false;
}
async _deleteOne(deleteId, isFile, batchIndex, selectedItems, item, onProgress) {
let retryCount = 0;
const maxRetries = 3;
const label = isFile ? "文件" : "目录";
while (retryCount <= maxRetries) {
try {
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
"fid[0]": deleteId,
ignore_warn: "1"
});
if (result && result.state) {
return 1;
} else {
const errMsg = (result && result.error) || "未知错误";
if (retryCount < maxRetries && this.isRateLimited(result)) {
retryCount++;
const backoff = (retryCount + 1) * 2000 + Math.random() * 1000;
onProgress && onProgress(batchIndex + 1, selectedItems.length, item, `风控触发,${backoff / 1000}秒后重试(${retryCount}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, backoff));
continue;
}
console.error(`删除${label}失败: ${deleteId}`, errMsg);
return 0;
}
} catch (e) {
if (retryCount < maxRetries && this.isRateLimited(e)) {
retryCount++;
const backoff = (retryCount + 1) * 2000 + Math.random() * 1000;
onProgress && onProgress(batchIndex + 1, selectedItems.length, item, `风控触发,${backoff / 1000}秒后重试(${retryCount}/${maxRetries})`);
await new Promise(resolve => setTimeout(resolve, backoff));
continue;
}
console.error(`删除${label}失败: ${deleteId}`, e);
return 0;
}
}
return 0;
}
showDeleteDialog(carNum2, matchList, afterDelete) {
const upperCarNum = carNum2.toUpperCase();
const safeMatchList = matchList.filter(m => m.name.toUpperCase().includes(upperCarNum));
const {regex: safeRegex} = codeParse(carNum2);
const safeMatchList = matchList.filter(m => safeRegex.test(m.name));
const unsafeCount = matchList.length - safeMatchList.length;
if (safeMatchList.length === 0) {
show.error(`作品 "${carNum2}" 匹配到的 ${matchList.length} 个项目均不含番号,无法安全删除。请手动在115中删除。`);
@@ -494,13 +567,32 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
let loadObj = loading();
let deletedCount = 0;
try {
const uniqueDirIds = [...new Set(selected.map((i) => safeMatchList[i].dirId))];
for (const dirId of uniqueDirIds) {
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
"fid[0]": dirId,
ignore_warn: "1"
});
result && result.state ? deletedCount++ : console.error(`删除目录失败: ${dirId}`, result);
const selectedItems = selected.map((i) => safeMatchList[i]);
const totalByDir = {};
for (const m of safeMatchList) {
totalByDir[m.dirId] = (totalByDir[m.dirId] || 0) + 1;
}
const groupedByDir = {};
for (const item of selectedItems) {
if (!groupedByDir[item.dirId]) groupedByDir[item.dirId] = [];
groupedByDir[item.dirId].push(item);
}
for (const [dirId, items] of Object.entries(groupedByDir)) {
if (items.length === totalByDir[dirId]) {
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
"fid[0]": dirId,
ignore_warn: "1"
});
result && result.state ? deletedCount++ : console.error(`删除目录失败: ${dirId}`, result);
} else {
for (const item of items) {
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
"fid[0]": item.folderId,
ignore_warn: "1"
});
result && result.state ? deletedCount++ : console.error(`删除文件失败: ${item.folderId}`, result);
}
}
}
loadObj.close();
show.ok(`作品 "${carNum2}" 已删除 ${deletedCount} 个文件夹`);
@@ -530,20 +622,76 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
window.open("https://115.com");
}
async searchFiles(carNum2) {
return (await this._searchFilesWithPhase(carNum2)).results;
}
async _searchFilesWithPhase(carNum2) {
const {regex} = codeParse(carNum2);
const searchKeyword = carNum2.toLowerCase().replace("fc2-", "");
const mapAndFilter = (rawData) => {
return rawData.map((data => ({
folderId: data.fid,
dirId: data.cid,
videoId: data.pc,
name: data.n,
createTime: utils.formatDate(new Date(1e3 * data.te)),
size: data.s,
isVideo: true
}))).filter((x => x.folderId && regex.test(x.name)));
};
var _a2;
let searchKeyword = carNum2.toLowerCase().replace("fc2-", "");
return (null == (_a2 = (await searchFiles(searchKeyword)).data) ? void 0 : _a2.map((data => ({
folderId: data.fid,
dirId: data.cid,
videoId: data.pc,
name: data.n,
createTime: utils.formatDate(new Date(1e3 * data.te)),
size: data.s,
isVideo: [ ".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv" ].some((ext => {
var _a3;
return null == (_a3 = data.n) ? void 0 : _a3.toLowerCase().endsWith(ext);
}))
}))).filter((x => x.folderId && x.isVideo && x.name.toLowerCase().includes(searchKeyword)))) || [];
const preciseData = (null == (_a2 = (await searchFiles(searchKeyword, 0, 30, {type: 4})).data) ? void 0 : _a2) || [];
const preciseResults = mapAndFilter(preciseData);
if (preciseResults.length > 0) return {results: preciseResults, usedPhase2: false};
const numberPart = extractNumberPart(carNum2);
if (numberPart === searchKeyword) return {results: preciseResults, usedPhase2: false};
const fuzzyData = (null == (_a2 = (await searchFiles(numberPart, 0, 30, {type: 4})).data) ? void 0 : _a2) || [];
return {results: mapAndFilter(fuzzyData), usedPhase2: true};
}
buildRenameTarget(carNum, originalName) {
const {regex} = codeParse(carNum);
const match = regex.exec(originalName);
if (!match) return null;
const before = originalName.slice(0, match.index);
const after = originalName.slice(match.index + match[0].length);
return before + carNum + after;
}
async autoRenameFiles(carNum, matchList) {
try {
const enableAutoRename = await storageManager.getSetting("enable115AutoRename", NO);
if (enableAutoRename !== YES) return;
const scope = await storageManager.getSetting("autoRenameScope", "vr_only");
if (scope === "vr_only" && !carNum.toUpperCase().includes("VR")) return;
const renameObj = {};
matchList.forEach(match => {
const newName = this.buildRenameTarget(carNum, match.name);
if (newName && newName !== match.name) {
renameObj[match.folderId] = newName;
}
});
if (Object.keys(renameObj).length === 0) return;
const formData = {};
Object.entries(renameObj).forEach(([fid, newName]) => {
formData[`files_new_name[${fid}]`] = newName;
});
const result = await gmHttp.postForm("https://webapi.115.com/files/batch_rename", formData);
if (result && result.state) {
Object.entries(renameObj).forEach(([fid, newName]) => {
const match = matchList.find(m => m.folderId === fid);
console.log(`[115重命名] ${match.name} → ${newName}`);
});
} else {
console.warn("[115重命名] 重命名失败:", (result && result.error) || "未知错误");
}
} catch (e) {
console.warn("[115重命名] 重命名失败,跳过:", e.message);
}
}
showMatchError($box2, carNum2, error) {
$box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn' data-keyword="${carNum2}" \n title="匹配失败,点击重试">匹配失败</a>`);
@@ -608,5 +756,25 @@ __publicField(_WangPan115MatchPlugin, "LoginStatus", {
let WangPan115MatchPlugin = _WangPan115MatchPlugin;
const extractNumberPart = (carNum) => {
let cleaned = carNum.replace(/^fc2[-_]?(ppv[-_])?/i, "");
const match = cleaned.match(/(\d+)$/);
return match ? match[1] : cleaned;
};
const codeParse = (code) => {
const codes = code.split(/-|_/);
const sep = "\\s?(0|-|_){0,2}\\s?";
let pattern = codes.join(sep);
if (/^fc2/i.test(code)) pattern = `${codes[0]}${sep}(ppv)?${sep}${codes.at(-1)}`;
return {
code,
codes,
prefix: codes[0],
regex: new RegExp(`(?<![a-z])${pattern}(?!\\d)`, "i"),
};
};
export { WangPan115Plugin, WangPan115MatchPlugin };
+5 -8
View File
@@ -6,10 +6,10 @@ export default defineConfig({
monkey({
entry: 'src/main.js',
userscript: {
name: 'JAV-JHS',
namespace: 'JAV-JHS',
version: '3.3.7',
author: 'xie bro,fireinrain',
name: 'JHS-enhance',
namespace: 'JHS-enhance',
version: '3.3.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',
@@ -38,7 +38,6 @@ export default defineConfig({
],
require: [
'https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js',
'https://cdn.jsdelivr.net/npm/tabulator-tables@6.5.2/dist/js/tabulator.min.js',
'https://cdn.jsdelivr.net/npm/layui-layer@1.0.9/dist/layer.min.js',
'https://cdn.jsdelivr.net/npm/blueimp-md5@2.19.0/js/md5.min.js',
'https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js',
@@ -92,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',
},
},
}),