mirror of
https://github.com/fireinrain/JHS-enhance.git
synced 2026-09-28 23:56:36 +08:00
Compare commits
22 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d03a9d1462 | |||
| b7cfc8541c | |||
| ce59d82ed2 | |||
| 37cf4e1183 | |||
| 8829af0fc3 | |||
| 9db06fba3b | |||
| 06b2497ee1 | |||
| 50581be5a6 | |||
| e92991c365 | |||
| 03700906e5 | |||
| e61739bd17 | |||
| c7310ae830 | |||
| fb5120551c | |||
| 42bc3f624a | |||
| 13abbd03fb | |||
| 6dfebb50a3 | |||
| 7637d33dc6 | |||
| 3287afe7ea | |||
| 06f7059f1b | |||
| c4d77e728a | |||
| a8fc1cde8a | |||
| c8bcac5f98 |
@@ -2,7 +2,7 @@
|
||||
|
||||
> Jav-鉴黄师 油猴脚本 — 工程化重构版本
|
||||
|
||||
基于 `vite-plugin-monkey` 脚手架,将原 10,000+ 行单体脚本拆分为 49 个可维护的 ES Module 模块,支持本地开发热更新、一键构建。
|
||||
基于 `vite-plugin-monkey` 脚手架,将原 10,000+ 行单体脚本拆分为 56 个可维护的 ES Module 模块,支持本地开发热更新、一键构建。
|
||||
|
||||
---
|
||||
|
||||
@@ -23,31 +23,63 @@
|
||||
|
||||
---
|
||||
|
||||
## 自动部署
|
||||
|
||||
本项目使用 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 查看热播
|
||||
- 自动翻页
|
||||
|
||||
### 详情页增强
|
||||
|
||||
- **磁力排序与过滤(JavDB)**:按发布日期/文件大小/文件数量拖拽排序,勾选启用;高清/4K/字幕/无码多条件并集过滤,4K 磁力行高亮显示
|
||||
- 磁力链接高亮匹配已收藏/已下载状态
|
||||
- DMM 多画质预览视频
|
||||
- **多源视频预览**:Javxy 聚合 API 独立按钮(番号/识别码右侧),多源 fallback(JavTrailers / JavDB 等),HLS 支持、播放进度记忆、画质切换
|
||||
- **标题翻译**:Google 翻译,支持开关多次切换不失效
|
||||
- **字幕搜索**:合并迅雷 + SubTitleCat 为单一入口,支持 115 网盘直传、详情页预匹配目录、多目录选择上传、长目录名截断展示、字幕预览绿色字体
|
||||
- **多源预览图**:javfree / projectjav / javstore 三源切换,来源按钮绿色高亮,sessionStorage 缓存,错误提示弹窗内显示
|
||||
- 演员信息展示、相关清单、评论查看
|
||||
- 以图识图、第三方站点跳转
|
||||
|
||||
### 115 网盘集成
|
||||
|
||||
- **番号智能匹配**:双阶段搜索策略 — 第一阶段精准搜索(完整番号 + `type:4`),未命中时第二阶段模糊兜底(尾号 +
|
||||
正则匹配),支持不规则文件名(如 `MIVR00345-5.mp4`、`ABP001.mp4`)
|
||||
- **不规则文件名自动重命名**
|
||||
:检测到需第二阶段才能匹配的作品时,自动将文件重命名为标准番号格式(如 `mdvr00317-3.mp4` → `MDVR-317-3.mp4`
|
||||
),设置面板可开关 + 选择范围(仅VR / 全部)
|
||||
- 作品目录匹配与详情页预加载
|
||||
- 多目录选择上传字幕
|
||||
- 字幕一键直传至匹配目录
|
||||
- **作品删除增强**:删除整个文件夹(非仅视频),支持标记删除批量操作,删除后字幕目录列表联动;批量删除含风控保护(1~3s
|
||||
随机延迟 + 触发风控自动重试退避);删除安全检查使用 `codeParse` 正则匹配番号
|
||||
|
||||
### 数据管理
|
||||
|
||||
- IndexedDB 本地存储
|
||||
- 云盘备份/恢复(阿里云盘 / WebDAV)
|
||||
- 跨 Tab 数据同步(BroadcastChannel)
|
||||
|
||||
### 自动化任务
|
||||
|
||||
- 新作品检测
|
||||
- 收藏演员同步
|
||||
- 黑名单自动检测与更新
|
||||
- 后台定时任务调度
|
||||
|
||||
### 其他
|
||||
|
||||
- FC2 站点支持(含 123AV 补全)
|
||||
- MagnetHub 磁力搜索
|
||||
- JavTrailers 站点处理
|
||||
- 设置面板:快捷键配置、域名配置、缓存清理、标记删除列表、打赏作者(二维码/钱包地址可自定义)
|
||||
- 浏览历史、想看/已看列表
|
||||
- 多站点支持:JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers / SubTitleCat
|
||||
|
||||
---
|
||||
|
||||
@@ -59,63 +91,69 @@ git push → GitHub Actions 自动构建 → 发布 GitHub Release
|
||||
```
|
||||
JHS-enhance/
|
||||
├── src/
|
||||
│ ├── main.js # 入口文件:全局初始化、插件注册、页面启动
|
||||
│ ├── core/ # 基础设施层
|
||||
│ │ ├── constants.js # 全局常量、CSS 样式、页面类型判断
|
||||
│ │ ├── base-plugin.js # 插件基类(含 SVG 图标、页面信息解析)
|
||||
│ │ ├── plugin-manager.js # 插件管理器(注册、CSS/插件生命周期)
|
||||
│ │ ├── globals.js # 全局依赖挂载(jQuery、localforage 等)
|
||||
│ │ ├── storage.js # IndexedDB 存储管理器(localforage 封装)
|
||||
│ │ ├── utils.js # 工具函数集(DOM 操作、弹窗、快捷键等)
|
||||
│ │ └── gmHttp.js # GM_xmlhttpRequest 封装(带重试/并发控制)
|
||||
│ ├── api/ # API 层
|
||||
│ │ ├── javdb.js # JavDB API 封装(签名、搜索、评论等)
|
||||
│ │ └── dmm.js # DMM 预览视频获取(多画质)
|
||||
│ └── plugins/ # 插件层(40+ 个功能插件)
|
||||
│ ├── list-page.js # 列表页过滤与状态标签
|
||||
│ ├── list-page-button.js # 列表页功能按钮(待鉴定/已收藏/新作品/黑名单)
|
||||
│ ├── detail-page.js # 详情页数据加载
|
||||
│ ├── detail-page-button.js# 详情页操作按钮(含 115 目录预加载)
|
||||
│ ├── setting.js # 设置面板(Tabulator 表格)
|
||||
│ ├── translate.js # 标题翻译(Google 翻译 API)
|
||||
│ ├── preview-video.js # DMM 预览视频
|
||||
│ ├── highlight-magnet.js # 磁力链接高亮匹配
|
||||
│ ├── auto-page.js # 自动翻页 / 云盘备份
|
||||
│ ├── blacklist.js # 黑名单管理(演员/番号)
|
||||
│ ├── favorite-actresses.js# 收藏演员同步
|
||||
│ ├── new-video.js # 新作品检测
|
||||
│ ├── task.js # 后台任务调度
|
||||
│ ├── history.js # 浏览历史
|
||||
│ ├── wangpan-115.js # 115 网盘匹配
|
||||
│ ├── wangpan-115-task.js # 115 网盘任务处理
|
||||
│ ├── aliyun-pan.js # 阿里云盘
|
||||
│ ├── main.js # 入口文件:全局初始化、插件注册、页面启动、loading/Toast/Viewer 全局函数
|
||||
│ ├── core/ # 基础设施层(7 个模块)
|
||||
│ │ ├── constants.js # 全局常量、CSS 样式、页面类型判断、枚举值
|
||||
│ │ ├── base-plugin.js # 插件基类(含 SVG 图标、页面信息解析、getBean 跨插件调用)
|
||||
│ │ ├── plugin-manager.js # 插件管理器(注册、CSS 注入、插件生命周期、按站点分组)
|
||||
│ │ ├── globals.js # 全局依赖挂载(jQuery、localforage、layer 等)
|
||||
│ │ ├── storage.js # IndexedDB 存储管理器(localforage 封装,含缓存)
|
||||
│ │ ├── utils.js # 工具函数集(DOM 操作、弹窗确认、快捷键、重试、日期格式化)
|
||||
│ │ └── gmHttp.js # GM_xmlhttpRequest 封装(GET/POST/Form/File、超时重试、分块下载)
|
||||
│ ├── api/ # API 层(4 个模块)
|
||||
│ │ ├── javdb.js # JavDB API 封装(签名算法、搜索、评论、磁力列表)
|
||||
│ │ ├── dmm.js # DMM 预览视频获取(多画质、默认画质选择)
|
||||
│ │ ├── dmm-graphql.js # DMM GraphQL API(备选视频源回退)
|
||||
│ │ └── javxy.js # Javxy 聚合视频 API(多源解析、签名生成)
|
||||
│ ├── lib/ # 第三方补丁库
|
||||
│ │ ├── gm-xhr-parallel.js # Tampermonkey MV3 并行请求修复(redirect: manual)
|
||||
│ │ └── hls-runtime.js # HLS 视频播放运行时(hls.js 封装、进度记忆、画质切换)
|
||||
│ └── plugins/ # 插件层(42 个功能插件)
|
||||
│ ├── list-page.js # 列表页过滤与状态标签渲染
|
||||
│ ├── list-page-button.js # 列表页功能按钮(待鉴定/已收藏/新作品/黑名单管理)
|
||||
│ ├── detail-page.js # 详情页数据加载与基础增强
|
||||
│ ├── detail-page-button.js# 详情页操作按钮(115 目录预加载 + 磁力排序过滤面板)
|
||||
│ ├── setting.js # 设置面板(Tab 页:基础/任务/域名/快捷键/屏蔽/缓存/标记删除/打赏作者;含 115 自动重命名开关 + 范围选择)
|
||||
│ ├── translate.js # 标题翻译(Google 翻译 API,原始标题缓存)
|
||||
│ ├── preview-video.js # DMM 预览视频播放器
|
||||
│ ├── highlight-magnet.js # 磁力链接高亮 + 排序 + 过滤(多键排序、并集过滤、4K 高亮)
|
||||
│ ├── auto-page.js # 自动翻页
|
||||
│ ├── blacklist.js # 黑名单管理(屏蔽演员/番号/关键词)
|
||||
│ ├── favorite-actresses.js# 收藏演员同步与高亮
|
||||
│ ├── new-video.js # 新作品检测与通知
|
||||
│ ├── task.js # 后台任务调度(黑名单/收藏演员/新作品定时执行)
|
||||
│ ├── history.js # 浏览历史表格
|
||||
│ ├── wangpan-115.js # 115 网盘匹配/标签/删除(含 codeParse 正则匹配、双阶段搜索、自动重命名)
|
||||
│ ├── wangpan-115-task.js # 115 网盘任务处理(文件搜索 API 封装)
|
||||
│ ├── aliyun-pan.js # 阿里云盘备份/恢复
|
||||
│ ├── fc2.js # FC2 页面处理
|
||||
│ ├── fc2-by123av.js # FC2 通过 123AV 搜索
|
||||
│ ├── actress-info.js # 演员信息展示
|
||||
│ ├── related.js # 相关清单
|
||||
│ ├── review.js # 评论展示
|
||||
│ ├── cover-button.js # 封面按钮
|
||||
│ ├── fold-category.js # 分类折叠
|
||||
│ ├── hit-show.js # 热播榜单
|
||||
│ ├── fc2-by123av.js # FC2 通过 123AV 补全番号信息
|
||||
│ ├── actress-info.js # 演员信息展示(含头像、生日、三围等)
|
||||
│ ├── related.js # 相关作品清单
|
||||
│ ├── review.js # 评论区展示
|
||||
│ ├── cover-button.js # 封面快捷按钮(SVG 图标 + 5 种封面操作)
|
||||
│ ├── fold-category.js # 分类折叠展开
|
||||
│ ├── hit-show.js # 热播榜单 + 免 VIP 查看
|
||||
│ ├── top250.js # Top250 排行榜
|
||||
│ ├── nav-bar.js # JavDB 导航栏增强
|
||||
│ ├── bus-nav-bar.js # JavBus 导航栏增强
|
||||
│ ├── bus-detail-page.js # JavBus 详情页
|
||||
│ ├── bus-detail-page.js # JavBus 详情页专属处理
|
||||
│ ├── bus-img.js # JavBus 图片处理
|
||||
│ ├── bus-preview-video.js # JavBus 预览视频
|
||||
│ ├── image-recognition.js # 以图识图
|
||||
│ ├── screenshot.js # 多源预览图(javfree/projectjav/javstore)
|
||||
│ ├── subtitles.js # 字幕搜索(整合迅雷+SubTitleCat)+ 115 直传
|
||||
│ ├── req115.js # 115 网盘 API 封装(Req + Drive115 + Req115)
|
||||
│ ├── image-recognition.js # 以图识图(第三方识图接口)
|
||||
│ ├── screenshot.js # 多源预览图(javfree/projectjav/javstore + 来源切换弹窗)
|
||||
│ ├── subtitles.js # 字幕搜索(整合迅雷+SubTitleCat)+ 115 多目录直传
|
||||
│ ├── req115.js # 115 网盘 API 封装(Req 基类 + Drive115 + Req115)
|
||||
│ ├── magnet-hub.js # MagnetHub 磁力搜索
|
||||
│ ├── other-site.js # 第三方站点链接
|
||||
│ ├── jav-trailers.js # JavTrailers 站点
|
||||
│ ├── subtitle-cat.js # SubTitleCat 字幕搜索(已整合到 subtitles.js,保留文件)
|
||||
│ ├── other-site.js # 第三方站点跳转链接
|
||||
│ ├── jav-trailers.js # JavTrailers 站点处理
|
||||
│ ├── javxy-preview-video.js# Javxy 多源视频预览(独立按钮、聚合 API、HLS 支持)
|
||||
│ ├── subtitle-cat.js # SubTitleCat 字幕搜索(已整合,保留原文件)
|
||||
│ ├── filter-title-keyword.js # 标题关键词过滤
|
||||
│ └── want-watched.js # 想看/已看视频列表
|
||||
├── vite.config.mjs # Vite + vite-plugin-monkey 配置
|
||||
├── vite.config.mjs # Vite + vite-plugin-monkey 配置(元数据/CDN 依赖/域名匹配)
|
||||
├── package.json
|
||||
└── JHS-origin.js # 原始单体脚本(参考)
|
||||
└── JHS-origin.js # 原始单体脚本(参考归档)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -245,6 +283,12 @@ window.show.ok('操作成功'); // Toast 通知
|
||||
|
||||
---
|
||||
|
||||
## 赞助
|
||||
|
||||
如果当前项目有帮到您的话,赞助我喝瓶水/Star项目也是极好的,感谢支持 :)
|
||||
|
||||

|
||||
|
||||
## 构建配置
|
||||
|
||||
`vite-plugin-monkey` 在 `vite.config.mjs` 中配置了:
|
||||
@@ -258,7 +302,37 @@ window.show.ok('操作成功'); // Toast 通知
|
||||
|
||||
## 版本历史
|
||||
|
||||
- **v3.3.6** — 工程化重构版本,模块化拆分,支持 HMR 开发
|
||||
- **v3.3.9**
|
||||
- 新增:115 番号智能匹配 — `codeParse` 正则匹配引擎,双阶段搜索策略(精准 →
|
||||
模糊兜底),支持不规则文件名(如 `MIVR00345-5.mp4`、`ABP001.mp4`)
|
||||
- 新增:115 不规则文件名自动重命名 —
|
||||
检测到需模糊匹配的作品时自动批量重命名为标准格式(如 `mdvr00317-3.mp4` → `MDVR-317-3.mp4`),设置面板可开关 +
|
||||
范围选择(仅VR / 全部)
|
||||
- 优化:删除安全检查改用 `codeParse` 正则匹配,消除 `includes` 方式对不规则文件名的误判
|
||||
- 优化:`searchFiles` 增加 `type:4` 参数,搜索仅返回视频文件,避免目录占用 30 条槽位
|
||||
|
||||
- **v3.3.8**
|
||||
- 新增:Javxy 多源视频预览独立按钮(番号/识别码右侧),基于 Javxy 聚合 API,支持 JavTrailers / JavDB 等多源回退
|
||||
- 新增:HLS 视频播放运行时(hls.js 封装),支持 .m3u8 格式、播放进度记忆、画质切换
|
||||
- 新增:DMM GraphQL API 模块,作为备选视频源回退方案
|
||||
- 优化:`bus-preview-video.js` 和 `preview-video.js` 清理移植代码,保持原有播放逻辑不变
|
||||
- 优化:`dmm.js` 回退至仅 DMM Affiliate API + GraphQL API 双通道,移除 Javxy 耦合
|
||||
|
||||
- **v3.3.7**
|
||||
- 新增:JavDB 磁力排序与过滤(拖拽排序、多条件并集过滤、4K 高亮)
|
||||
- 新增:多源预览图(javfree/projectjav/javstore + 来源切换弹窗 + 绿色高亮)
|
||||
- 新增:字幕搜索合并为单一入口(迅雷+SubTitleCat),详情页预匹配 115 目录,多目录选择上传
|
||||
- 新增:标记删除批量 115 删除(含风控保护:1~3s 随机延迟 + 风控自动退避重试)
|
||||
- 优化:作品删除改为删除整个文件夹(非仅视频),删除后字幕目录列表联动更新
|
||||
- 修复:标题翻译开关多次切换后失效(原始标题 data-original-title 缓存)
|
||||
- 修复:115 目录名过长截断展示(最多 26 字符)
|
||||
- 修复:字幕预览区域字体改为绿色
|
||||
- 修复:JavBus/JavDB 磁力列表 DOM 精确提取(data-rank/data-size/data-files/data-date 属性)
|
||||
- 修复:无码过滤条件新增 -U/-UC/-u/-uc 匹配
|
||||
- 优化:删除作品 115 风控提示(标记删除面板)
|
||||
- 优化:设置面板打赏作者二维码/钱包地址可自定义
|
||||
|
||||
- **v3.3.6** — 工程化重构版本,模块化拆分(41 个插件),支持 Vite HMR 开发热更新
|
||||
|
||||
---
|
||||
|
||||
|
||||
Vendored
+15019
-11297
File diff suppressed because one or more lines are too long
@@ -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
@@ -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 };
|
||||
@@ -0,0 +1,223 @@
|
||||
import {qualityOptions} from '../core/constants.js';
|
||||
|
||||
const JAVXY_TOKEN = [118, 119, 112, 71, 97, 110, 28, 84, 124, 65, 76, 102, 65, 16, 77, 109, 64, 82, 85, 83, 67, 92, 125, 108, 83, 65, 124, 107, 84, 104, 71, 84, 17, 124, 118, 125, 104, 8, 125, 96, 112, 103, 29, 18, 82, 83, 87, 84]
|
||||
.map(v => String.fromCharCode(v ^ 0x25)).join('');
|
||||
|
||||
const JAVXY_ENDPOINTS = [
|
||||
{host: String.fromCharCode(106, 97, 118, 120, 121, 46, 99, 99, 46, 99, 100), label: 'Javxy'},
|
||||
{
|
||||
host: String.fromCharCode(119, 111, 114, 107, 101, 114, 46, 106, 97, 118, 120, 121, 46, 99, 99, 46, 99, 100),
|
||||
label: 'Javxy Worker'
|
||||
},
|
||||
];
|
||||
|
||||
const JAVXY_SOURCE_LABELS = {
|
||||
'Tokyo-Hot': 'Javxy | Tokyo-Hot',
|
||||
FC2: 'Javxy | FC2',
|
||||
Direct: 'Javxy | Direct',
|
||||
DMM: 'Javxy | dmm',
|
||||
MGStage: 'Javxy | MGStage',
|
||||
DUGA: 'Javxy | DUGA',
|
||||
MYWIFE: 'Javxy | MyWife',
|
||||
JavTrailers: 'Javxy | JavTrailers',
|
||||
JavDB: 'Javxy | Javdb',
|
||||
AVWikiDB: 'Javxy | AVWikiDB',
|
||||
JAVDatabase: 'Javxy | JAVDatabase',
|
||||
HEYZO: 'Javxy | Heyzo',
|
||||
HeyDouga: 'Javxy | HeyDouga',
|
||||
PACO: 'Javxy | Paco',
|
||||
'10MU': 'Javxy | 10mu',
|
||||
Caribbean: 'Javxy | 加勒比',
|
||||
'1Pondo': 'Javxy | 一本道',
|
||||
};
|
||||
|
||||
const JAVXY_QUALITY_OPTIONS = [
|
||||
{quality: '4k', text: '4K'},
|
||||
{quality: 'hhb', text: '1080p'},
|
||||
{quality: '1080p', text: '1080p'},
|
||||
{quality: 'hmb', text: '720p'},
|
||||
{quality: '720p', text: '720p'},
|
||||
{quality: 'mhb', text: '576p'},
|
||||
{quality: '540p', text: '540p'},
|
||||
{quality: 'mmb', text: '432p'},
|
||||
{quality: '480p', text: '480p'},
|
||||
{quality: '396p', text: '396p'},
|
||||
{quality: '360p', text: '360p'},
|
||||
{quality: '240p', text: '240p'},
|
||||
];
|
||||
|
||||
const jpSourceFailedKey = (source = 'DMM') => `javxy_jp_source_failed_until_${String(source || '').toUpperCase()}`;
|
||||
|
||||
const isJpSourceTemporarilyFailed = (source = 'DMM') => {
|
||||
const until = Number(sessionStorage.getItem(jpSourceFailedKey(source)) || 0);
|
||||
if (!Number.isFinite(until) || until <= Date.now()) {
|
||||
sessionStorage.removeItem(jpSourceFailedKey(source));
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const markJpSourceTemporarilyFailed = (source = 'DMM') => {
|
||||
sessionStorage.setItem(jpSourceFailedKey(source), String(Date.now() + 30 * 60 * 1000));
|
||||
};
|
||||
|
||||
const selectHighestQuality = (qualityMap) => {
|
||||
const rank = new Map(JAVXY_QUALITY_OPTIONS.map((item, index) => [item.quality, index]));
|
||||
return Object.keys(qualityMap || {})
|
||||
.filter(key => qualityMap[key])
|
||||
.sort((a, b) => (rank.get(a) ?? -1) - (rank.get(b) ?? -1))[0] || null;
|
||||
};
|
||||
|
||||
const sortQualityKeys = (qualityMap) => {
|
||||
const rank = new Map(JAVXY_QUALITY_OPTIONS.map((item, index) => [item.quality, index]));
|
||||
return Object.keys(qualityMap || {})
|
||||
.filter(key => qualityMap[key])
|
||||
.sort((a, b) => (rank.get(a) ?? -1) - (rank.get(b) ?? -1));
|
||||
};
|
||||
|
||||
const normalizeJavxySource = (value) => {
|
||||
const raw = String(value || '').trim().toLowerCase();
|
||||
if (raw.includes('fc2')) return 'FC2';
|
||||
if (raw.includes('mgstage')) return 'MGStage';
|
||||
if (raw.includes('heydouga')) return 'Direct';
|
||||
if (raw.includes('mywife')) return 'MYWIFE';
|
||||
if (raw.includes('duga')) return 'DUGA';
|
||||
if (raw.includes('javtrailers')) return 'JavTrailers';
|
||||
if (raw.includes('javdb')) return 'JavDB';
|
||||
if (raw.includes('avwikidb')) return 'AVWikiDB';
|
||||
if (raw.includes('javdatabase')) return 'JAVDatabase';
|
||||
if (raw.includes('dmm')) return 'DMM';
|
||||
if (raw === 'direct' || raw.includes('heyzo') || raw.includes('heydouga') || raw.includes('paco') || raw.includes('10musume') || raw.includes('10mu') || raw.includes('1pondo') || raw.includes('caribbean') || raw.includes('tokyo-hot') || raw.includes('tokyohot')) return 'Direct';
|
||||
return String(value || '').trim();
|
||||
};
|
||||
|
||||
const javxyQualityMapToDmmFormat = (javxyQualityMap) => {
|
||||
const result = {};
|
||||
const qualityKeys = qualityOptions.map(o => o.quality);
|
||||
|
||||
for (const [key, url] of Object.entries(javxyQualityMap || {})) {
|
||||
if (!url) continue;
|
||||
const lowerKey = key.toLowerCase();
|
||||
|
||||
if (qualityKeys.includes(key)) {
|
||||
result[key] = url;
|
||||
} else if (qualityKeys.includes(lowerKey)) {
|
||||
result[lowerKey] = url;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
const fromJavxyCcCd = async (id, rawCode = '', options = {}) => {
|
||||
const query = String(id || rawCode || '').trim();
|
||||
if (!query) {
|
||||
clog.debug('Javxy 跳过:查询词为空');
|
||||
return null;
|
||||
}
|
||||
|
||||
for (const endpoint of JAVXY_ENDPOINTS) {
|
||||
const params = new URLSearchParams({client: 'laosiji-new'});
|
||||
if (Array.isArray(options.skip) && options.skip.length) params.set('skip', options.skip.join(','));
|
||||
if (Array.isArray(options.prefer) && options.prefer.length) params.set('prefer', options.prefer.join(','));
|
||||
if (Array.isArray(options.source) && options.source.length) params.set('source', options.source.join(','));
|
||||
if (options.playbackFallback) params.set('purpose', 'playback-fallback');
|
||||
|
||||
const apiUrl = `https://${endpoint.host}/trailers/${encodeURIComponent(query)}?${params}`;
|
||||
clog.debug('Javxy 请求 API', {query, apiUrl, endpoint: endpoint.label});
|
||||
|
||||
let r;
|
||||
try {
|
||||
r = await gmHttp.get(apiUrl, null, {
|
||||
'Accept': 'application/json,text/plain,*/*',
|
||||
'X-Javxy-Token': JAVXY_TOKEN,
|
||||
}, {timeout: 8000});
|
||||
} catch (e) {
|
||||
clog.debug('Javxy API 网络失败,尝试下一个节点', {endpoint: endpoint.label, error: e.message});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!r) {
|
||||
clog.debug('Javxy API 无响应,尝试下一个节点', {endpoint: endpoint.label});
|
||||
continue;
|
||||
}
|
||||
|
||||
const trailerUrl = String(r?.trailer || '').trim();
|
||||
if (!trailerUrl) {
|
||||
clog.debug('Javxy 无 trailer 字段', {endpoint: endpoint.label, keys: Object.keys(r || {})});
|
||||
return null;
|
||||
}
|
||||
|
||||
const qualityMap = r?.qualities && typeof r.qualities === 'object' ? r.qualities : {};
|
||||
const quality = r?.quality && qualityMap[r.quality] ? r.quality : selectHighestQuality(qualityMap);
|
||||
const sourceBase = JAVXY_SOURCE_LABELS[r?.source] || `Javxy | ${r?.source || 'dmm'}`;
|
||||
const directUrl = qualityMap[quality] || trailerUrl;
|
||||
|
||||
clog.debug('Javxy 返回结果', {
|
||||
endpoint: endpoint.label,
|
||||
source: r?.source,
|
||||
quality,
|
||||
qualities: Object.keys(qualityMap),
|
||||
url: directUrl,
|
||||
});
|
||||
|
||||
const dmmQualityMap = javxyQualityMapToDmmFormat(qualityMap);
|
||||
|
||||
return {
|
||||
url: directUrl,
|
||||
source: sourceBase,
|
||||
type: String(r?.type || '').trim() || 'video',
|
||||
qualities: qualityMap,
|
||||
quality,
|
||||
directUrl,
|
||||
code: id,
|
||||
rawCode,
|
||||
javxySource: String(r?.source || '').trim(),
|
||||
requiresJP: Boolean(r?.requiresJP),
|
||||
urls: Array.isArray(r?.urls) && r.urls.length ? r.urls : sortQualityKeys(qualityMap).map(key => qualityMap[key]),
|
||||
dmmQualityMap,
|
||||
};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const fallbackJavxyResult = async (code, rawCode = '', failedSources = [], options = {}) => {
|
||||
const skip = [...new Set((failedSources || []).map(source => normalizeJavxySource(source)).filter(Boolean))];
|
||||
const source = [...new Set((options.source || []).map(s => String(s || '').trim()).filter(Boolean))];
|
||||
|
||||
if (!skip.length && !source.length) return null;
|
||||
|
||||
clog.debug('Javxy 播放失败回落查询', {code, skip, source});
|
||||
|
||||
return fromJavxyCcCd(code, rawCode, {skip, source, playbackFallback: true});
|
||||
};
|
||||
|
||||
const getJavxyVideoUrls = async (code, failedSources = []) => {
|
||||
const result = await fallbackJavxyResult(code, code, failedSources, {
|
||||
source: ['JavTrailers', 'JavDB'],
|
||||
});
|
||||
|
||||
if (result?.dmmQualityMap && Object.keys(result.dmmQualityMap).length > 0) {
|
||||
return result.dmmQualityMap;
|
||||
}
|
||||
|
||||
if (result?.url) {
|
||||
const qualityMap = {};
|
||||
qualityMap['720p'] = result.url;
|
||||
return qualityMap;
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export {
|
||||
fromJavxyCcCd,
|
||||
fallbackJavxyResult,
|
||||
getJavxyVideoUrls,
|
||||
normalizeJavxySource,
|
||||
isJpSourceTemporarilyFailed,
|
||||
markJpSourceTemporarilyFailed,
|
||||
JAVXY_TOKEN,
|
||||
JAVXY_ENDPOINTS,
|
||||
};
|
||||
@@ -569,6 +569,198 @@ const mainCss = `
|
||||
background: rgba(59, 130, 246, 0.9);
|
||||
box-shadow: 0 4px 16px rgba(59, 130, 246, 0.4);
|
||||
}
|
||||
|
||||
.magnet-4k-highlight {
|
||||
background-color: #fff5f5 !important;
|
||||
}
|
||||
.magnet-4k-highlight .name {
|
||||
color: #f40 !important;
|
||||
}
|
||||
|
||||
.msf-overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(0, 0, 0, 0.3);
|
||||
z-index: 999997;
|
||||
}
|
||||
.msf-panel {
|
||||
position: fixed;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
background: #fff;
|
||||
border-radius: 10px;
|
||||
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.2);
|
||||
z-index: 999998;
|
||||
width: 380px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.msf-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid #e5e7eb;
|
||||
background: #f9fafb;
|
||||
border-radius: 10px 10px 0 0;
|
||||
}
|
||||
.msf-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: #1e293b;
|
||||
}
|
||||
.msf-close {
|
||||
font-size: 22px;
|
||||
color: #9ca3af;
|
||||
cursor: pointer;
|
||||
padding: 0 6px;
|
||||
line-height: 1;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.msf-close:hover {
|
||||
color: #ef4444;
|
||||
}
|
||||
.msf-body {
|
||||
padding: 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
}
|
||||
.msf-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
.msf-section-title {
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: #6b7280;
|
||||
}
|
||||
.msf-sort-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
.msf-sort-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
cursor: default;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
.msf-sort-item:hover {
|
||||
border-color: #cbd5e1;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.msf-sort-item.msf-dragging {
|
||||
opacity: 0.5;
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
}
|
||||
.msf-sort-item.msf-drag-over {
|
||||
border-color: #3b82f6;
|
||||
background: #eff6ff;
|
||||
box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
.msf-drag-handle {
|
||||
color: #cbd5e1;
|
||||
font-size: 16px;
|
||||
cursor: grab;
|
||||
line-height: 1;
|
||||
letter-spacing: -2px;
|
||||
transition: color 0.2s;
|
||||
}
|
||||
.msf-drag-handle:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
.msf-sort-item:hover .msf-drag-handle {
|
||||
color: #94a3b8;
|
||||
}
|
||||
.msf-sort-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
}
|
||||
.msf-sort-checkbox {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.msf-sort-hint {
|
||||
font-size: 12px;
|
||||
color: #9ca3af;
|
||||
margin-left: 4px;
|
||||
}
|
||||
.msf-filter-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
.msf-filter-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 14px;
|
||||
background: #f8fafc;
|
||||
border: 1px solid #e2e8f0;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
color: #374151;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
.msf-filter-label:hover {
|
||||
border-color: #cbd5e1;
|
||||
background: #f1f5f9;
|
||||
}
|
||||
.msf-filter-checkbox {
|
||||
margin: 0;
|
||||
cursor: pointer;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
.msf-footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
padding: 14px 18px;
|
||||
border-top: 1px solid #e5e7eb;
|
||||
}
|
||||
.msf-btn {
|
||||
padding: 8px 22px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
}
|
||||
.msf-btn-cancel {
|
||||
background: #f3f4f6;
|
||||
color: #374151;
|
||||
}
|
||||
.msf-btn-cancel:hover {
|
||||
background: #e5e7eb;
|
||||
}
|
||||
.msf-btn-confirm {
|
||||
background: #3b82f6;
|
||||
color: #fff;
|
||||
}
|
||||
.msf-btn-confirm:hover {
|
||||
background: #2563eb;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
|
||||
|
||||
+9
-3
@@ -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
@@ -98,7 +98,7 @@ class Utils {
|
||||
}
|
||||
if (!viewerExists) {
|
||||
this.layerIndexStack.pop();
|
||||
layer.close(topLayerIndex);
|
||||
typeof layer !== 'undefined' && layer.close(topLayerIndex);
|
||||
}
|
||||
}
|
||||
setupEscClose(layerIndex) {
|
||||
|
||||
@@ -0,0 +1,266 @@
|
||||
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
|
||||
|
||||
const HLS_SCRIPT_URL = 'https://cdn.jsdelivr.net/npm/hls.js@1.5.18/dist/hls.min.js';
|
||||
|
||||
let hlsLoadPromise = null;
|
||||
|
||||
const getHlsClass = () => {
|
||||
const HlsClass = window.Hls || globalThis.Hls || (typeof Hls !== 'undefined' ? Hls : null);
|
||||
return HlsClass && HlsClass.isSupported && HlsClass.isSupported() ? HlsClass : null;
|
||||
};
|
||||
|
||||
const binaryTextToArrayBuffer = (value) => {
|
||||
const text = String(value || '');
|
||||
const bytes = new Uint8Array(text.length);
|
||||
for (let index = 0; index < text.length; index += 1) {
|
||||
bytes[index] = text.charCodeAt(index) & 0xff;
|
||||
}
|
||||
return bytes.buffer;
|
||||
};
|
||||
|
||||
const loadHlsLibrary = () => {
|
||||
const readyHls = getHlsClass();
|
||||
if (readyHls) return Promise.resolve(readyHls);
|
||||
if (hlsLoadPromise) return hlsLoadPromise;
|
||||
|
||||
const loadByGm = () => new Promise((resolve) => {
|
||||
GM_xmlhttpRequest({
|
||||
method: 'GET',
|
||||
url: HLS_SCRIPT_URL,
|
||||
timeout: 15000,
|
||||
onload: (r) => {
|
||||
if (r.status >= 200 && r.status < 300 && r.responseText) {
|
||||
try {
|
||||
Function(`${r.responseText}\n//# sourceURL=${HLS_SCRIPT_URL}`).call(globalThis);
|
||||
} catch (err) {
|
||||
clog.error('HLS: hls.js 执行失败', err);
|
||||
}
|
||||
}
|
||||
resolve(getHlsClass());
|
||||
},
|
||||
onerror: () => resolve(getHlsClass()),
|
||||
ontimeout: () => resolve(getHlsClass()),
|
||||
});
|
||||
});
|
||||
|
||||
hlsLoadPromise = new Promise((resolve) => {
|
||||
const existing = document.querySelector('script[data-jhs-hls="1"]');
|
||||
if (existing) {
|
||||
existing.addEventListener('load', () => resolve(getHlsClass()), {once: true});
|
||||
existing.addEventListener('error', () => loadByGm().then(resolve), {once: true});
|
||||
setTimeout(() => {
|
||||
if (!getHlsClass()) loadByGm().then(resolve);
|
||||
}, 4000);
|
||||
return;
|
||||
}
|
||||
const hlsScript = document.createElement('script');
|
||||
hlsScript.src = HLS_SCRIPT_URL;
|
||||
hlsScript.async = true;
|
||||
hlsScript.dataset.jhsHls = '1';
|
||||
hlsScript.onload = () => resolve(getHlsClass());
|
||||
hlsScript.onerror = () => loadByGm().then(resolve);
|
||||
document.head.appendChild(hlsScript);
|
||||
}).then((HlsClass) => {
|
||||
if (!HlsClass) hlsLoadPromise = null;
|
||||
return HlsClass;
|
||||
});
|
||||
return hlsLoadPromise;
|
||||
};
|
||||
|
||||
const createHlsLoader = () => {
|
||||
class GMHlsLoader {
|
||||
constructor(config) {
|
||||
this.config = config;
|
||||
this.context = null;
|
||||
this.callbacks = null;
|
||||
this.loader = null;
|
||||
this.stats = this.createStats();
|
||||
}
|
||||
|
||||
createStats() {
|
||||
return {
|
||||
aborted: false,
|
||||
loaded: 0,
|
||||
retry: 0,
|
||||
total: 0,
|
||||
chunkCount: 0,
|
||||
bwEstimate: 0,
|
||||
trequest: 0,
|
||||
tfirst: 0,
|
||||
tload: 0,
|
||||
loading: {start: 0, first: 0, end: 0},
|
||||
parsing: {start: 0, end: 0},
|
||||
buffering: {start: 0, first: 0, end: 0},
|
||||
};
|
||||
}
|
||||
|
||||
destroy() {
|
||||
this.abort();
|
||||
}
|
||||
|
||||
abort() {
|
||||
if (this.stats) this.stats.aborted = true;
|
||||
if (this.loader) {
|
||||
try {
|
||||
this.loader.abort();
|
||||
} catch (e) { /* ignore */
|
||||
}
|
||||
}
|
||||
this.loader = null;
|
||||
}
|
||||
|
||||
load(context, config, callbacks) {
|
||||
this.context = context;
|
||||
this.callbacks = callbacks;
|
||||
const requestUrl = context.url;
|
||||
const wantsArrayBuffer =
|
||||
context.responseType === 'arraybuffer' ||
|
||||
/\.(?:ts|m4s|mp4|key)(?:[?#]|$)/i.test(requestUrl);
|
||||
const startedAt = performance.now();
|
||||
const stats = (this.stats = this.createStats());
|
||||
stats.trequest = startedAt;
|
||||
stats.tfirst = startedAt;
|
||||
stats.tload = startedAt;
|
||||
stats.loading.start = startedAt;
|
||||
|
||||
this.loader = GM_xmlhttpRequest({
|
||||
method: 'GET',
|
||||
url: requestUrl,
|
||||
responseType: 'text',
|
||||
overrideMimeType: wantsArrayBuffer ? 'text/plain; charset=x-user-defined' : undefined,
|
||||
timeout: config && config.timeout ? config.timeout : 20000,
|
||||
headers: {
|
||||
Accept: wantsArrayBuffer
|
||||
? '*/*'
|
||||
: 'application/vnd.apple.mpegurl, application/x-mpegURL, */*',
|
||||
},
|
||||
onprogress: (event) => {
|
||||
stats.loaded = Number(event && event.loaded ? event.loaded : stats.loaded || 0);
|
||||
stats.total = Number(event && event.total ? event.total : stats.total || stats.loaded || 0);
|
||||
if (!stats.loading.first && stats.loaded > 0) {
|
||||
stats.loading.first = performance.now();
|
||||
}
|
||||
},
|
||||
onload: (r) => {
|
||||
const status = Number(r.status || 0);
|
||||
const response = {
|
||||
code: status,
|
||||
text: r.statusText || '',
|
||||
url: r.finalUrl || requestUrl,
|
||||
};
|
||||
stats.tfirst = stats.tfirst || performance.now();
|
||||
stats.tload = performance.now();
|
||||
stats.loading.first = stats.loading.first || stats.tload;
|
||||
stats.loading.end = stats.tload;
|
||||
if (status < 200 || status >= 300) {
|
||||
callbacks.onError && callbacks.onError(response, context, null, stats);
|
||||
return;
|
||||
}
|
||||
const responseText = r.responseText || r.response || '';
|
||||
const data = wantsArrayBuffer
|
||||
? binaryTextToArrayBuffer(responseText)
|
||||
: responseText;
|
||||
stats.loaded = data && data.byteLength ? data.byteLength : (data && data.length ? data.length : stats.loaded || 0);
|
||||
stats.total = stats.total || stats.loaded;
|
||||
stats.bwEstimate =
|
||||
stats.loading.end > stats.loading.first
|
||||
? Math.round((stats.total * 8000) / (stats.loading.end - stats.loading.first))
|
||||
: 0;
|
||||
callbacks.onSuccess && callbacks.onSuccess({data, url: response.url}, stats, context, response);
|
||||
},
|
||||
onerror: () => {
|
||||
callbacks.onError &&
|
||||
callbacks.onError({code: 0, text: 'network error', url: requestUrl}, context, null, stats);
|
||||
},
|
||||
ontimeout: () => {
|
||||
stats.tload = performance.now();
|
||||
stats.loading.end = stats.tload;
|
||||
callbacks.onTimeout && callbacks.onTimeout(stats, context, null);
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return GMHlsLoader;
|
||||
};
|
||||
|
||||
const isM3U8Url = (url) => /\.m3u8(?:[?#].*)?$/i.test(String(url || ''));
|
||||
|
||||
const attachHlsToVideo = (videoEl, src) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
if (!videoEl || !src) {
|
||||
reject(new Error('video element or source missing'));
|
||||
return;
|
||||
}
|
||||
|
||||
const HlsClass = getHlsClass();
|
||||
if (!HlsClass) {
|
||||
videoEl.src = src;
|
||||
videoEl.load && videoEl.load();
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const hls = new HlsClass({
|
||||
enableWorker: false,
|
||||
lowLatencyMode: true,
|
||||
loader: createHlsLoader(),
|
||||
autoStartLoad: true,
|
||||
startPosition: 0,
|
||||
capLevelToPlayerSize: true,
|
||||
testBandwidth: false,
|
||||
preferManagedMediaSource: false,
|
||||
maxBufferLength: 6,
|
||||
maxMaxBufferLength: 12,
|
||||
backBufferLength: 30,
|
||||
maxBufferHole: 0.5,
|
||||
nudgeOffset: 0.1,
|
||||
manifestLoadingMaxRetry: 2,
|
||||
levelLoadingMaxRetry: 2,
|
||||
fragLoadingMaxRetry: 2,
|
||||
manifestLoadingTimeOut: 12000,
|
||||
levelLoadingTimeOut: 12000,
|
||||
fragLoadingTimeOut: 12000,
|
||||
abrEwmaFastLive: 3,
|
||||
abrEwmaSlowLive: 9,
|
||||
});
|
||||
|
||||
hls.on(HlsClass.Events.MANIFEST_PARSED, () => {
|
||||
hls.startLoad(0);
|
||||
videoEl.play().catch(() => {
|
||||
});
|
||||
resolve(true);
|
||||
});
|
||||
|
||||
hls.on(HlsClass.Events.ERROR, (_, data) => {
|
||||
if (!data || !data.fatal) return;
|
||||
clog.error('HLS: 播放失败', data);
|
||||
if (data.type === HlsClass.ErrorTypes.MEDIA_ERROR) {
|
||||
if (videoEl.readyState >= 2) {
|
||||
try {
|
||||
hls.recoverMediaError && hls.recoverMediaError();
|
||||
} catch (e) { /* ignore */
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
reject(new Error(data.details || 'HLS fatal error'));
|
||||
});
|
||||
|
||||
hls.loadSource(src);
|
||||
hls.attachMedia(videoEl);
|
||||
videoEl._hls = hls;
|
||||
});
|
||||
};
|
||||
|
||||
const destroyHls = (videoEl) => {
|
||||
if (videoEl && videoEl._hls) {
|
||||
try {
|
||||
videoEl._hls.destroy();
|
||||
} catch (e) { /* ignore */
|
||||
}
|
||||
videoEl._hls = null;
|
||||
}
|
||||
};
|
||||
|
||||
export {loadHlsLibrary, getHlsClass, createHlsLoader, isM3U8Url, attachHlsToVideo, destroyHls, HLS_SCRIPT_URL};
|
||||
+26
-21
@@ -29,6 +29,7 @@ import { TOP250Plugin } from './plugins/top250.js';
|
||||
import { NavBarPlugin } from './plugins/nav-bar.js';
|
||||
import { BusDetailPagePlugin } from './plugins/bus-detail-page.js';
|
||||
import { BusPreviewVideoPlugin } from './plugins/bus-preview-video.js';
|
||||
import {JavxyPreviewVideoPlugin} from './plugins/javxy-preview-video.js';
|
||||
import { ImageRecognitionPlugin } from './plugins/image-recognition.js';
|
||||
import { BusNavBarPlugin } from './plugins/bus-nav-bar.js';
|
||||
import { RelatedPlugin } from './plugins/related.js';
|
||||
@@ -819,29 +820,31 @@ window.ImageHoverPreview = class {
|
||||
}));
|
||||
}();
|
||||
|
||||
const originalLayerClose = layer.close;
|
||||
if (typeof layer !== 'undefined') {
|
||||
const originalLayerClose = layer.close;
|
||||
|
||||
layer.close = function(index) {
|
||||
const result = originalLayerClose.call(this, index);
|
||||
!function(waitTime = 10) {
|
||||
setTimeout((() => {
|
||||
const openLayerCount = document.querySelectorAll(".layui-layer-shade").length;
|
||||
document.documentElement.style.overflow = openLayerCount > 0 ? "hidden" : "";
|
||||
}), waitTime);
|
||||
}();
|
||||
return result;
|
||||
};
|
||||
|
||||
const originalLayerOpen = layer.open;
|
||||
|
||||
layer.open = function(options) {
|
||||
const originalSuccess = (options = options || {}).success;
|
||||
options.success = function(layero, index) {
|
||||
"function" == typeof originalSuccess && originalSuccess.call(this, layero, index);
|
||||
utils.setupEscClose(index);
|
||||
layer.close = function (index) {
|
||||
const result = originalLayerClose.call(this, index);
|
||||
!function (waitTime = 10) {
|
||||
setTimeout((() => {
|
||||
const openLayerCount = document.querySelectorAll(".layui-layer-shade").length;
|
||||
document.documentElement.style.overflow = openLayerCount > 0 ? "hidden" : "";
|
||||
}), waitTime);
|
||||
}();
|
||||
return result;
|
||||
};
|
||||
return originalLayerOpen.call(this, options);
|
||||
};
|
||||
|
||||
const originalLayerOpen = layer.open;
|
||||
|
||||
layer.open = function (options) {
|
||||
const originalSuccess = (options = options || {}).success;
|
||||
options.success = function (layero, index) {
|
||||
"function" == typeof originalSuccess && originalSuccess.call(this, layero, index);
|
||||
utils.setupEscClose(index);
|
||||
};
|
||||
return originalLayerOpen.call(this, options);
|
||||
};
|
||||
}
|
||||
|
||||
utils.importResource("https://cdn.jsdelivr.net/npm/layui-layer@1.0.9/layer.min.css");
|
||||
|
||||
@@ -876,6 +879,7 @@ const pluginManager = function() {
|
||||
pluginManager2.register(DetailPageButtonPlugin);
|
||||
pluginManager2.register(HighlightMagnetPlugin);
|
||||
pluginManager2.register(PreviewVideoPlugin);
|
||||
pluginManager2.register(JavxyPreviewVideoPlugin);
|
||||
pluginManager2.register(FilterTitleKeywordPlugin);
|
||||
pluginManager2.register(ActressInfoPlugin);
|
||||
pluginManager2.register(OtherSitePlugin);
|
||||
@@ -906,6 +910,7 @@ const pluginManager = function() {
|
||||
pluginManager2.register(FilterTitleKeywordPlugin);
|
||||
pluginManager2.register(HighlightMagnetPlugin);
|
||||
pluginManager2.register(BusPreviewVideoPlugin);
|
||||
pluginManager2.register(JavxyPreviewVideoPlugin);
|
||||
pluginManager2.register(MagnetHubPlugin);
|
||||
pluginManager2.register(ScreenShotPlugin);
|
||||
pluginManager2.register(OtherSitePlugin);
|
||||
|
||||
@@ -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
@@ -23,7 +23,7 @@ class DetailPageButtonPlugin extends BasePlugin {
|
||||
}
|
||||
async createMenuBtn() {
|
||||
const pageInfo = this.getPageInfo(), carNum2 = pageInfo.carNum,
|
||||
buttonsHtml = '\n <div style="margin: 10px auto; display: flex; justify-content: space-between; align-items: center; flex-wrap:wrap;gap: 20px;">\n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="filterBtn" class="menu-btn" style="width: 120px; background-color:#de3333; color: white; text-align: center; padding: 8px 0;">\n <span>🚫 屏蔽</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="width: 120px; background-color:#25b1dc; color: white; text-align: center; padding: 8px 0;">\n <span>⭐ 收藏</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="width: 120px; background-color:#7bc73b; color: white; text-align: center; padding: 8px 0;">\n <span>📥️ 已下载</span>\n </a>\n <a id="hasWatchBtn" class="menu-btn" style="width: 120px; background-color:#d7a80c; color: white; text-align: center; padding: 8px 0;">\n <span>🔍 已观看</span>\n </a>\n </div>\n \n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="enable-magnets-filter" class="menu-btn" style="width: 140px; background-color: #c2bd4c; color: white; text-align: center; padding: 8px 0;">\n <span id="magnets-span">关闭磁力过滤</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n <a id="subtitleSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕搜索</span>\n </a>\n <!--\n <a id="xunLeiSubtitleBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="search-subtitle-btn" class="menu-btn" style="width: 160px; background: linear-gradient(to bottom, #8d5656, rgb(196,159,91)); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (SubTitleCat)</span>\n </a>\n -->\n </div>\n </div>\n ';
|
||||
buttonsHtml = '\n <div style="margin: 10px auto; display: flex; justify-content: space-between; align-items: center; flex-wrap:wrap;gap: 20px;">\n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="filterBtn" class="menu-btn" style="width: 120px; background-color:#de3333; color: white; text-align: center; padding: 8px 0;">\n <span>🚫 屏蔽</span>\n </a>\n <a id="favoriteBtn" class="menu-btn" style="width: 120px; background-color:#25b1dc; color: white; text-align: center; padding: 8px 0;">\n <span>⭐ 收藏</span>\n </a>\n <a id="hasDownBtn" class="menu-btn" style="width: 120px; background-color:#7bc73b; color: white; text-align: center; padding: 8px 0;">\n <span>📥️ 已下载</span>\n </a>\n <a id="hasWatchBtn" class="menu-btn" style="width: 120px; background-color:#d7a80c; color: white; text-align: center; padding: 8px 0;">\n <span>🔍 已观看</span>\n </a>\n </div>\n \n <div style="display: flex; gap: 10px; flex-wrap:wrap;">\n <a id="enable-magnets-filter" class="menu-btn" style="width: 140px; background-color: #c2bd4c; color: white; text-align: center; padding: 8px 0;">\n <span id="magnets-span">排序与过滤</span>\n </a>\n <a id="magnetSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to right, rgb(245,140,1), rgb(84,161,29)); color: white; text-align: center; padding: 8px 0;">\n <span>磁力搜索</span>\n </a>\n <a id="subtitleSearchBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕搜索</span>\n </a>\n <!--\n <a id="xunLeiSubtitleBtn" class="menu-btn" style="width: 120px; background: linear-gradient(to left, #375f7c, #2196F3); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (迅雷)</span>\n </a>\n <a id="search-subtitle-btn" class="menu-btn" style="width: 160px; background: linear-gradient(to bottom, #8d5656, rgb(196,159,91)); color: white; text-align: center; padding: 8px 0;">\n <span>字幕 (SubTitleCat)</span>\n </a>\n -->\n </div>\n </div>\n ';
|
||||
isJavDb && $(".tabs").after(buttonsHtml);
|
||||
isJavBus && $("#mag-submit-show").before(buttonsHtml);
|
||||
$("#favoriteBtn").on("click", (() => this.favoriteOne()));
|
||||
@@ -43,19 +43,45 @@ class DetailPageButtonPlugin extends BasePlugin {
|
||||
}
|
||||
});
|
||||
}));
|
||||
const highlightMagnetPlugin = this.getBean("HighlightMagnetPlugin"), enableMagnetsFilter = await storageManager.getSetting("enableMagnetsFilter", YES);
|
||||
$("#magnets-span").text(enableMagnetsFilter === YES ? "关闭磁力过滤" : "开启磁力过滤");
|
||||
enableMagnetsFilter === YES && highlightMagnetPlugin.doFilterMagnet();
|
||||
const highlightMagnetPlugin = this.getBean("HighlightMagnetPlugin"),
|
||||
enableMagnetsFilter = await storageManager.getSetting("enableMagnetsFilter", NO);
|
||||
if (enableMagnetsFilter === YES) {
|
||||
const savedSettings = await storageManager.getSetting("magnetSortAndFilter", "{}");
|
||||
let settings;
|
||||
try {
|
||||
settings = JSON.parse(savedSettings);
|
||||
} catch (e) {
|
||||
settings = {};
|
||||
}
|
||||
const sortEnabled = settings.sortEnabled || {};
|
||||
const defaultFullSortOrder = isJavBus ? ["date", "size"] : ["date", "size", "files"];
|
||||
const fullSortOrder = (settings.sortOrder && settings.sortOrder.length === defaultFullSortOrder.length) ? settings.sortOrder : defaultFullSortOrder;
|
||||
const effectiveSortOrder = fullSortOrder.filter(key => sortEnabled[key]);
|
||||
const applySettings = {
|
||||
filterHD: settings.filterHD || !1,
|
||||
filter4K: settings.filter4K || !1,
|
||||
filterSubtitle: settings.filterSubtitle || !1,
|
||||
filterUncensored: settings.filterUncensored || !1,
|
||||
sortOrder: effectiveSortOrder,
|
||||
};
|
||||
const rowReadyCondition = isJavBus
|
||||
? (() => $("#magnet-table tr").length > 1)
|
||||
: (() => $("#magnets-content .item[data-rank]").length > 0);
|
||||
utils.loopDetector(rowReadyCondition, () => {
|
||||
highlightMagnetPlugin.applySortAndFilter(applySettings);
|
||||
});
|
||||
$("#magnets-span").text("取消磁力过滤");
|
||||
} else {
|
||||
$("#magnets-span").text("排序与过滤");
|
||||
}
|
||||
$("#enable-magnets-filter").on("click", (event => {
|
||||
let $span = $("#magnets-span");
|
||||
if ("关闭磁力过滤" === $span.text()) {
|
||||
const $span = $("#magnets-span");
|
||||
if ("取消磁力过滤" === $span.text()) {
|
||||
highlightMagnetPlugin.showAll();
|
||||
$span.text("开启磁力过滤");
|
||||
$span.text("排序与过滤");
|
||||
storageManager.saveSettingItem("enableMagnetsFilter", NO);
|
||||
} else {
|
||||
highlightMagnetPlugin.doFilterMagnet();
|
||||
$span.text("关闭磁力过滤");
|
||||
storageManager.saveSettingItem("enableMagnetsFilter", YES);
|
||||
this.openMagnetSortFilterPanel(highlightMagnetPlugin);
|
||||
}
|
||||
}));
|
||||
$("#subtitleSearchBtn").on("click", (() => {
|
||||
@@ -66,6 +92,195 @@ class DetailPageButtonPlugin extends BasePlugin {
|
||||
// $("#xunLeiSubtitleBtn").on("click", (() => this.searchXunLeiSubtitle(carNum2)));
|
||||
this.showStatus(carNum2).then();
|
||||
}
|
||||
|
||||
async openMagnetSortFilterPanel(highlightMagnetPlugin) {
|
||||
const existingPanel = document.getElementById("magnet-sort-filter-panel");
|
||||
if (existingPanel) {
|
||||
existingPanel.remove();
|
||||
return;
|
||||
}
|
||||
const savedSettings = await storageManager.getSetting("magnetSortAndFilter", "{}");
|
||||
let settings;
|
||||
try {
|
||||
settings = JSON.parse(savedSettings);
|
||||
} catch (e) {
|
||||
settings = {};
|
||||
}
|
||||
const sortEnabled = settings.sortEnabled || (isJavBus ? {date: !1, size: !1} : {date: !1, size: !1, files: !1});
|
||||
const defaultSortOrder = isJavBus ? ["date", "size"] : ["date", "size", "files"];
|
||||
const sortOrder = (settings.sortOrder && settings.sortOrder.length === defaultSortOrder.length) ? settings.sortOrder : defaultSortOrder;
|
||||
const filterHD = settings.filterHD || !1;
|
||||
const filter4K = settings.filter4K || !1;
|
||||
const filterSubtitle = settings.filterSubtitle || !1;
|
||||
const filterUncensored = settings.filterUncensored || !1;
|
||||
const SORT_LABELS = isJavBus ? {date: "发布日期", size: "文件大小"} : {
|
||||
date: "发布日期",
|
||||
size: "文件大小",
|
||||
files: "文件数量"
|
||||
};
|
||||
const SORT_HINTS = isJavBus ? {date: "从新到旧", size: "从大到小"} : {
|
||||
date: "从新到旧",
|
||||
size: "从大到小",
|
||||
files: "从少到多"
|
||||
};
|
||||
const FILTER_ITEMS = [
|
||||
{key: "HD", label: "高清", checked: filterHD},
|
||||
{key: "4K", label: "4K", checked: filter4K},
|
||||
{key: "Subtitle", label: "字幕", checked: filterSubtitle},
|
||||
{key: "Uncensored", label: "无码", checked: filterUncensored},
|
||||
];
|
||||
const currentSortOrder = [...sortOrder];
|
||||
const currentSortEnabled = {...sortEnabled};
|
||||
const currentFilters = {
|
||||
HD: filterHD,
|
||||
"4K": filter4K,
|
||||
Subtitle: filterSubtitle,
|
||||
Uncensored: filterUncensored,
|
||||
};
|
||||
const buildSortItemsHTML = () => {
|
||||
return currentSortOrder.map(key => {
|
||||
const checked = currentSortEnabled[key] ? "checked" : "";
|
||||
return `<div class="msf-sort-item" draggable="true" data-sort-key="${key}">
|
||||
<span class="msf-drag-handle">⋮⋮</span>
|
||||
<label class="msf-sort-label">
|
||||
<input type="checkbox" class="msf-sort-checkbox" data-sort-key="${key}" ${checked}>
|
||||
${SORT_LABELS[key]} <span class="msf-sort-hint">${SORT_HINTS[key]}</span>
|
||||
</label>
|
||||
</div>`;
|
||||
}).join("");
|
||||
};
|
||||
const buildFilterItemsHTML = () => {
|
||||
return FILTER_ITEMS.map(item => {
|
||||
const checked = currentFilters[item.key] ? "checked" : "";
|
||||
return `<label class="msf-filter-label">
|
||||
<input type="checkbox" class="msf-filter-checkbox" data-filter-key="${item.key}" ${checked}>
|
||||
${item.label}
|
||||
</label>`;
|
||||
}).join("");
|
||||
};
|
||||
const panelHTML = `
|
||||
<div class="msf-overlay" id="msf-overlay"></div>
|
||||
<div class="msf-panel" id="magnet-sort-filter-panel">
|
||||
<div class="msf-header">
|
||||
<span class="msf-title">磁力排序与过滤</span>
|
||||
<span class="msf-close" id="msf-close-btn">×</span>
|
||||
</div>
|
||||
<div class="msf-body">
|
||||
<div class="msf-section">
|
||||
<div class="msf-section-title">排序优先级(拖动调整顺序,勾选启用)</div>
|
||||
<div class="msf-sort-list" id="msf-sort-list">
|
||||
${buildSortItemsHTML()}
|
||||
</div>
|
||||
</div>
|
||||
<div class="msf-section">
|
||||
<div class="msf-section-title">过滤条件(可多选,并集关系)</div>
|
||||
<div class="msf-filter-list">
|
||||
${buildFilterItemsHTML()}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="msf-footer">
|
||||
<button class="msf-btn msf-btn-cancel" id="msf-cancel-btn">取消</button>
|
||||
<button class="msf-btn msf-btn-confirm" id="msf-confirm-btn">确认</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
const wrapper = document.createElement("div");
|
||||
wrapper.innerHTML = panelHTML;
|
||||
document.body.appendChild(wrapper);
|
||||
const panel = document.getElementById("magnet-sort-filter-panel");
|
||||
const overlay = document.getElementById("msf-overlay");
|
||||
const closePanel = () => {
|
||||
panel.remove();
|
||||
overlay.remove();
|
||||
};
|
||||
overlay.addEventListener("click", closePanel);
|
||||
document.getElementById("msf-close-btn").addEventListener("click", closePanel);
|
||||
document.getElementById("msf-cancel-btn").addEventListener("click", closePanel);
|
||||
const rebuildSortList = () => {
|
||||
const sortList = document.getElementById("msf-sort-list");
|
||||
sortList.innerHTML = buildSortItemsHTML();
|
||||
bindSortEvents();
|
||||
};
|
||||
const bindSortEvents = () => {
|
||||
const sortItems = document.querySelectorAll("#msf-sort-list .msf-sort-item");
|
||||
sortItems.forEach(item => {
|
||||
item.addEventListener("dragstart", (e) => {
|
||||
e.dataTransfer.effectAllowed = "move";
|
||||
e.dataTransfer.setData("text/plain", item.dataset.sortKey);
|
||||
item.classList.add("msf-dragging");
|
||||
});
|
||||
item.addEventListener("dragend", () => {
|
||||
item.classList.remove("msf-dragging");
|
||||
document.querySelectorAll("#msf-sort-list .msf-sort-item").forEach(el => el.classList.remove("msf-drag-over"));
|
||||
});
|
||||
item.addEventListener("dragover", (e) => {
|
||||
e.preventDefault();
|
||||
e.dataTransfer.dropEffect = "move";
|
||||
item.classList.add("msf-drag-over");
|
||||
});
|
||||
item.addEventListener("dragleave", () => {
|
||||
item.classList.remove("msf-drag-over");
|
||||
});
|
||||
item.addEventListener("drop", (e) => {
|
||||
e.preventDefault();
|
||||
item.classList.remove("msf-drag-over");
|
||||
const fromKey = e.dataTransfer.getData("text/plain");
|
||||
const toKey = item.dataset.sortKey;
|
||||
if (fromKey === toKey) return;
|
||||
const fromIdx = currentSortOrder.indexOf(fromKey);
|
||||
const toIdx = currentSortOrder.indexOf(toKey);
|
||||
currentSortOrder.splice(fromIdx, 1);
|
||||
currentSortOrder.splice(toIdx, 0, fromKey);
|
||||
rebuildSortList();
|
||||
});
|
||||
const checkbox = item.querySelector(".msf-sort-checkbox");
|
||||
checkbox.addEventListener("change", () => {
|
||||
currentSortEnabled[item.dataset.sortKey] = checkbox.checked;
|
||||
});
|
||||
});
|
||||
};
|
||||
bindSortEvents();
|
||||
document.querySelectorAll(".msf-filter-checkbox").forEach(cb => {
|
||||
cb.addEventListener("change", () => {
|
||||
currentFilters[cb.dataset.filterKey] = cb.checked;
|
||||
});
|
||||
});
|
||||
document.getElementById("msf-confirm-btn").addEventListener("click", async () => {
|
||||
try {
|
||||
const enabledSortOrder = currentSortOrder.filter(key => currentSortEnabled[key]);
|
||||
const newSettings = {
|
||||
sortOrder: currentSortOrder,
|
||||
sortEnabled: currentSortEnabled,
|
||||
filterHD: currentFilters.HD,
|
||||
filter4K: currentFilters["4K"],
|
||||
filterSubtitle: currentFilters.Subtitle,
|
||||
filterUncensored: currentFilters.Uncensored,
|
||||
};
|
||||
await storageManager.saveSettingItem("magnetSortAndFilter", JSON.stringify(newSettings));
|
||||
const hasAnyFilter = currentFilters.HD || currentFilters["4K"] || currentFilters.Subtitle || currentFilters.Uncensored;
|
||||
const hasAnySort = enabledSortOrder.length > 0;
|
||||
if (hasAnyFilter || hasAnySort) {
|
||||
await highlightMagnetPlugin.applySortAndFilter({
|
||||
filterHD: currentFilters.HD,
|
||||
filter4K: currentFilters["4K"],
|
||||
filterSubtitle: currentFilters.Subtitle,
|
||||
filterUncensored: currentFilters.Uncensored,
|
||||
sortOrder: enabledSortOrder,
|
||||
});
|
||||
$("#magnets-span").text("取消磁力过滤");
|
||||
await storageManager.saveSettingItem("enableMagnetsFilter", YES);
|
||||
} else {
|
||||
highlightMagnetPlugin.showAll();
|
||||
$("#magnets-span").text("排序与过滤");
|
||||
await storageManager.saveSettingItem("enableMagnetsFilter", NO);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[磁力排序] 应用失败:", e);
|
||||
}
|
||||
closePanel();
|
||||
});
|
||||
}
|
||||
async showStatus(carNum2) {
|
||||
const $filterBtn = $("#filterBtn span"), $favoriteBtn = $("#favoriteBtn span"), $hasDownBtn = $("#hasDownBtn span"), $hasWatchBtn = $("#hasWatchBtn span"), hotKeyDisplay = hotKey => hotKey ? `(${hotKey})` : "";
|
||||
$filterBtn.text(`🚫 屏蔽 ${hotKeyDisplay(this.filterHotKey)}`);
|
||||
|
||||
+202
-23
@@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,239 @@
|
||||
import {BasePlugin} from '../core/base-plugin.js';
|
||||
import {__publicField, isJavBus, isJavDb, qualityOptions} from '../core/constants.js';
|
||||
import {isM3U8Url, attachHlsToVideo, destroyHls, loadHlsLibrary, getHlsClass} from '../lib/hls-runtime.js';
|
||||
import {getJavxyVideoUrls} from '../api/javxy.js';
|
||||
|
||||
class JavxyPreviewVideoPlugin extends BasePlugin {
|
||||
getName() {
|
||||
return "JavxyPreviewVideoPlugin";
|
||||
}
|
||||
|
||||
_attachVideoSrc(videoEl, src) {
|
||||
if (!videoEl || !src) return;
|
||||
destroyHls(videoEl);
|
||||
|
||||
if (isM3U8Url(src)) {
|
||||
const HlsClass = getHlsClass();
|
||||
if (HlsClass) {
|
||||
attachHlsToVideo(videoEl, src).catch((err) => {
|
||||
clog.error('视频预览 HLS 加载失败:', err);
|
||||
videoEl.src = src;
|
||||
videoEl.load && videoEl.load();
|
||||
});
|
||||
} else {
|
||||
loadHlsLibrary().then((HlsClass) => {
|
||||
if (videoEl && videoEl.isConnected && HlsClass) {
|
||||
attachHlsToVideo(videoEl, src).catch((err) => {
|
||||
clog.error('视频预览 HLS 延迟加载失败:', err);
|
||||
videoEl.src = src;
|
||||
videoEl.load && videoEl.load();
|
||||
});
|
||||
} else if (videoEl && videoEl.isConnected) {
|
||||
videoEl.src = src;
|
||||
videoEl.load && videoEl.load();
|
||||
}
|
||||
});
|
||||
}
|
||||
} else {
|
||||
videoEl.src = src;
|
||||
videoEl.load && videoEl.load();
|
||||
}
|
||||
}
|
||||
|
||||
_selectDefaultQuality(qualityList, intendedDefault) {
|
||||
if (!qualityList || qualityList.length === 0) return null;
|
||||
const availableSet = new Set(qualityList);
|
||||
if (availableSet.has(intendedDefault)) return intendedDefault;
|
||||
const priorityOrder = qualityOptions.map(o => o.quality).reverse();
|
||||
for (const q of priorityOrder) if (availableSet.has(q)) return q;
|
||||
return qualityList[0];
|
||||
}
|
||||
|
||||
async initCss() {
|
||||
return `
|
||||
.jhs-javxy-video-modal {
|
||||
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
|
||||
background: rgba(0,0,0,0.95); z-index: 12345700;
|
||||
display: flex; justify-content: center; align-items: center;
|
||||
opacity: 0; visibility: hidden; transition: opacity 0.2s;
|
||||
}
|
||||
.jhs-javxy-video-modal.is-open { opacity: 1; visibility: visible; }
|
||||
.jhs-javxy-video-modal-inner {
|
||||
display: flex; flex-direction: column; align-items: center;
|
||||
gap: 12px; max-width: 90vw; max-height: 90vh;
|
||||
}
|
||||
.jhs-javxy-video-wrapper {
|
||||
width: 80vw; max-height: 80vh; aspect-ratio: 16/9;
|
||||
background: #000; position: relative;
|
||||
}
|
||||
.jhs-javxy-video-wrapper video {
|
||||
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
|
||||
}
|
||||
.jhs-javxy-quality-bar {
|
||||
display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;
|
||||
}
|
||||
.jhs-javxy-quality-btn {
|
||||
min-width: 60px; padding: 5px 10px; font-size: 13px;
|
||||
background: rgba(255,255,255,0.2); color: #fff;
|
||||
border: 1px solid rgba(255,255,255,0.5); border-radius: 4px;
|
||||
cursor: pointer; transition: background 0.2s;
|
||||
}
|
||||
.jhs-javxy-quality-btn:hover { background: rgba(255,255,255,0.4); }
|
||||
.jhs-javxy-quality-btn.active {
|
||||
background: #1890ff; border-color: #096dd9; font-weight: bold;
|
||||
}
|
||||
.jhs-javxy-loading {
|
||||
color: #fff; font-size: 16px; text-align: center;
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
async handle() {
|
||||
if (!window.isDetailPage) return;
|
||||
|
||||
const carNum = this.getPageInfo().carNum;
|
||||
if (!carNum) return;
|
||||
|
||||
this._addJavxyButton(carNum);
|
||||
}
|
||||
|
||||
_addJavxyButton(carNum) {
|
||||
let $target = null;
|
||||
|
||||
if (isJavDb) {
|
||||
$target = $('a[title="複製番號"]');
|
||||
} else if (isJavBus) {
|
||||
const headerSpans = document.querySelectorAll("span.header");
|
||||
for (const span of headerSpans) {
|
||||
if (span.textContent.trim() === "識別碼:") {
|
||||
const nextSpan = span.nextElementSibling;
|
||||
if (nextSpan && nextSpan.tagName === "SPAN") {
|
||||
const copyBtn = nextSpan.nextElementSibling;
|
||||
if (copyBtn && copyBtn.tagName === "BUTTON" && copyBtn.textContent.trim() === "复制") {
|
||||
$target = $(copyBtn);
|
||||
} else {
|
||||
$target = $(nextSpan);
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!$target || !$target.length) return;
|
||||
|
||||
const $btn = $(`<a class="site-btn" style="min-width: auto; padding: 0 5px; margin-left: 6px; margin-bottom: 0; background-color: #1890ff; vertical-align: middle; cursor: pointer; font-size: 11px;"><span>视频预览</span></a>`);
|
||||
$btn.on("click", async (e) => {
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
$btn.css("pointer-events", "none").find("span").text("...");
|
||||
try {
|
||||
await this._openJavxyPlayer(carNum);
|
||||
} finally {
|
||||
$btn.css("pointer-events", "").find("span").text("视频预览");
|
||||
}
|
||||
});
|
||||
$target.after($btn);
|
||||
}
|
||||
|
||||
async _openJavxyPlayer(carNum) {
|
||||
if ($("#jhs-javxy-video-modal").length === 0) {
|
||||
$("body").append(`
|
||||
<div id="jhs-javxy-video-modal" class="jhs-javxy-video-modal">
|
||||
<div class="jhs-javxy-video-modal-inner">
|
||||
<div class="jhs-javxy-video-wrapper">
|
||||
<video id="jhs-javxy-video" controls playsinline></video>
|
||||
</div>
|
||||
<div class="jhs-javxy-quality-bar"></div>
|
||||
</div>
|
||||
</div>
|
||||
`);
|
||||
const $modal = $("#jhs-javxy-video-modal");
|
||||
$modal.on("click", (e) => {
|
||||
if (e.target.id === "jhs-javxy-video-modal") {
|
||||
$modal.removeClass("is-open");
|
||||
const videoEl = document.getElementById("jhs-javxy-video");
|
||||
if (videoEl) {
|
||||
destroyHls(videoEl);
|
||||
videoEl.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
$(document).on("keydown", (e) => {
|
||||
if (e.key === "Escape" && $modal.hasClass("is-open")) {
|
||||
$modal.removeClass("is-open");
|
||||
const videoEl = document.getElementById("jhs-javxy-video");
|
||||
if (videoEl) {
|
||||
destroyHls(videoEl);
|
||||
videoEl.pause();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const $modal = $("#jhs-javxy-video-modal");
|
||||
const $wrapper = $modal.find(".jhs-javxy-video-wrapper");
|
||||
const $qualityBar = $modal.find(".jhs-javxy-quality-bar");
|
||||
$wrapper.html('<div class="jhs-javxy-loading">视频预览加载中...</div>');
|
||||
$qualityBar.empty();
|
||||
$modal.addClass("is-open");
|
||||
|
||||
let videoMap = null;
|
||||
try {
|
||||
videoMap = await getJavxyVideoUrls(carNum);
|
||||
} catch (err) {
|
||||
$wrapper.html(`<div class="jhs-javxy-loading">视频预览请求失败: ${err.message}</div>`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!videoMap || Object.keys(videoMap).length === 0) {
|
||||
$wrapper.html('<div class="jhs-javxy-loading">未找到可用的视频源</div>');
|
||||
return;
|
||||
}
|
||||
|
||||
const qualityList = Object.keys(videoMap);
|
||||
const defaultQuality = this._selectDefaultQuality(qualityList, '720p');
|
||||
const defaultUrl = videoMap[defaultQuality];
|
||||
|
||||
$wrapper.html(`<video id="jhs-javxy-video" controls playsinline>
|
||||
<source src="${defaultUrl}" />
|
||||
</video>`);
|
||||
const videoEl = document.getElementById("jhs-javxy-video");
|
||||
if (!videoEl) return;
|
||||
|
||||
if (isM3U8Url(defaultUrl)) {
|
||||
this._attachVideoSrc(videoEl, defaultUrl);
|
||||
}
|
||||
|
||||
let buttonsHtml = "";
|
||||
qualityOptions.forEach((option) => {
|
||||
const url = videoMap[option.quality];
|
||||
if (url) {
|
||||
buttonsHtml += `<button class="jhs-javxy-quality-btn${option.quality === defaultQuality ? ' active' : ''}"
|
||||
data-quality="${option.quality}" data-video-src="${url}">${option.text}</button>`;
|
||||
}
|
||||
});
|
||||
$qualityBar.html(buttonsHtml);
|
||||
|
||||
$qualityBar.off("click").on("click", ".jhs-javxy-quality-btn", (e) => {
|
||||
const $btn = $(e.currentTarget);
|
||||
if ($btn.hasClass("active")) return;
|
||||
const src = $btn.attr("data-video-src");
|
||||
const currentTime = videoEl.currentTime;
|
||||
destroyHls(videoEl);
|
||||
this._attachVideoSrc(videoEl, src);
|
||||
videoEl.load();
|
||||
if (currentTime > 0 && Number.isFinite(currentTime)) {
|
||||
videoEl.currentTime = currentTime;
|
||||
}
|
||||
videoEl.play().catch(() => {
|
||||
});
|
||||
$qualityBar.find(".jhs-javxy-quality-btn").removeClass("active");
|
||||
$btn.addClass("active");
|
||||
});
|
||||
|
||||
videoEl.play().catch((e) => console.warn("视频预览播放失败:", e));
|
||||
}
|
||||
}
|
||||
|
||||
export {JavxyPreviewVideoPlugin};
|
||||
@@ -402,11 +402,13 @@ class ListPagePlugin extends BasePlugin {
|
||||
}
|
||||
if (this.cache[carNum2]) {
|
||||
let _this = this;
|
||||
$title.attr("data-original-title", content);
|
||||
$title.contents().each((function() {
|
||||
3 === this.nodeType && "" !== this.textContent.trim() && (this.textContent = " " + _this.cache[carNum2] + " ");
|
||||
}));
|
||||
$title.attr("title", _this.cache[carNum2]);
|
||||
} else translateText(content).then((result => {
|
||||
$title.attr("data-original-title", content);
|
||||
if (isJavDb) {
|
||||
$title.contents().each((function() {
|
||||
3 !== this.nodeType || "" === this.textContent.trim() || this.textContent.includes(carNum2) || (this.textContent = " " + result + " ");
|
||||
@@ -424,7 +426,7 @@ class ListPagePlugin extends BasePlugin {
|
||||
async revertTranslation() {
|
||||
$(this.getSelector().itemSelector).toArray().forEach((ele => {
|
||||
let $box2 = $(ele);
|
||||
const originalContent = $box2.find(".box").attr("title") || $box2.find(".video-title").attr("title") || $box2.find("img").attr("data-title");
|
||||
const originalContent = $box2.find(".video-title").attr("data-original-title") || $box2.find(".box").attr("title") || $box2.find(".video-title").attr("title") || $box2.find("img").attr("data-title");
|
||||
let carNum2;
|
||||
isJavDb && (carNum2 = $box2.find(".video-title strong").text().trim());
|
||||
const $title = $box2.find(".video-title");
|
||||
|
||||
+45
-42
@@ -2,6 +2,30 @@ import { BasePlugin } from '../core/base-plugin.js';
|
||||
import { __publicField } from '../core/constants.js';
|
||||
import { GM_xmlhttpRequest } from 'vite-plugin-monkey/dist/client';
|
||||
|
||||
function showCopiedFeedback($btn) {
|
||||
const originalText = $btn.text();
|
||||
$btn.addClass("copied").text("已复制");
|
||||
setTimeout((() => {
|
||||
$btn.removeClass("copied").text(originalText);
|
||||
}), 2e3);
|
||||
}
|
||||
|
||||
function fallbackCopy(text, $btn) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
showCopiedFeedback($btn);
|
||||
} catch (err) {
|
||||
console.error("复制失败:", err);
|
||||
alert("复制失败,请手动复制链接");
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
|
||||
class MagnetHubPlugin extends BasePlugin {
|
||||
constructor() {
|
||||
super(...arguments);
|
||||
@@ -63,6 +87,27 @@ class MagnetHubPlugin extends BasePlugin {
|
||||
$(e.target).addClass("active");
|
||||
this.searchEngine($resultsContainer, this.currentEngine, keyword);
|
||||
}));
|
||||
$container.on("click", ".copy-btn", function () {
|
||||
const $btn = $(this), magnet = $btn.data("magnet");
|
||||
navigator.clipboard ? navigator.clipboard.writeText(magnet).then((() => {
|
||||
showCopiedFeedback($btn);
|
||||
})).catch((() => {
|
||||
fallbackCopy(magnet, $btn);
|
||||
})) : fallbackCopy(magnet, $btn);
|
||||
});
|
||||
$container.on("click", ".down-115", (async event => {
|
||||
event.stopPropagation();
|
||||
const magnet = $(event.currentTarget).data("magnet");
|
||||
let loadObj = loading();
|
||||
try {
|
||||
await this.getBean("WangPan115TaskPlugin").handleAddTask(magnet);
|
||||
} catch (e) {
|
||||
show.error("发生错误:" + e);
|
||||
console.error(e);
|
||||
} finally {
|
||||
loadObj.close();
|
||||
}
|
||||
}));
|
||||
this.searchEngine($resultsContainer, this.currentEngine || this.searchEngines[defaultEngineIndex], keyword);
|
||||
return $container;
|
||||
}
|
||||
@@ -96,49 +141,7 @@ class MagnetHubPlugin extends BasePlugin {
|
||||
const $result = $(`\n <div class="magnet-result">\n <div class="magnet-title"><a href="${result.magnet}">${result.title}</a></div>\n <div class="magnet-info">\n <span>大小: ${result.size || "未知"}</span>\n <span>日期: ${result.date || "未知"}</span>\n </div>\n <div class="magnet-copy">\n <button class="magnet-hub-btn copy-btn" data-magnet="${result.magnet}">复制链接</button>\n <button class="magnet-hub-btn down-115" data-magnet="${result.magnet}">115离线下载</button>\n </div>\n </div>\n `);
|
||||
$container.append($result);
|
||||
}));
|
||||
$container.on("click", ".copy-btn", (function() {
|
||||
const $btn = $(this), magnet = $btn.data("magnet");
|
||||
navigator.clipboard ? navigator.clipboard.writeText(magnet).then((() => {
|
||||
showCopiedFeedback($btn);
|
||||
})).catch((err => {
|
||||
fallbackCopy(magnet, $btn);
|
||||
})) : fallbackCopy(magnet, $btn);
|
||||
}));
|
||||
$container.on("click", ".down-115", (async event => {
|
||||
const magnet = $(event.currentTarget).data("magnet");
|
||||
let loadObj = loading();
|
||||
try {
|
||||
await this.getBean("WangPan115TaskPlugin").handleAddTask(magnet);
|
||||
} catch (e) {
|
||||
show.error("发生错误:" + e);
|
||||
console.error(e);
|
||||
} finally {
|
||||
loadObj.close();
|
||||
}
|
||||
}));
|
||||
} else $container.append('<div class="magnet-error">没有找到相关结果</div>');
|
||||
function showCopiedFeedback($btn) {
|
||||
const originalText = $btn.text();
|
||||
$btn.addClass("copied").text("已复制");
|
||||
setTimeout((() => {
|
||||
$btn.removeClass("copied").text(originalText);
|
||||
}), 2e3);
|
||||
}
|
||||
function fallbackCopy(text, $btn) {
|
||||
const textarea = document.createElement("textarea");
|
||||
textarea.value = text;
|
||||
textarea.style.position = "fixed";
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
try {
|
||||
document.execCommand("copy");
|
||||
showCopiedFeedback($btn);
|
||||
} catch (err) {
|
||||
console.error("复制失败:", err);
|
||||
alert("复制失败,请手动复制链接");
|
||||
}
|
||||
document.body.removeChild(textarea);
|
||||
}
|
||||
}
|
||||
parseBTSOW($container, engine, keyword, cacheKey) {
|
||||
const _this = this;
|
||||
|
||||
+95
-96
@@ -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};
|
||||
@@ -1,5 +1,6 @@
|
||||
import { BasePlugin } from '../core/base-plugin.js';
|
||||
import { __publicField, currentHref, NO, YES, qualityOptions } from '../core/constants.js';
|
||||
import {getDmmVideo, selectDefaultQuality} from '../api/dmm.js';
|
||||
|
||||
class PreviewVideoPlugin extends BasePlugin {
|
||||
getName() {
|
||||
@@ -49,7 +50,7 @@ class PreviewVideoPlugin extends BasePlugin {
|
||||
} else {
|
||||
clog.debug("JavDB没有视频播放元素, 开始创建...");
|
||||
const videoCoverSrc = $(".column-video-cover img").attr("src");
|
||||
$(".preview-images").prepend(`\n <a class="preview-video-container" data-fancybox="gallery" href="#preview-video">\n <span>預告片</span>\n <img src="${videoCoverSrc}" class="video-cover" style="width: 150px; height: auto;" alt="">\n </a>\n `);
|
||||
$(".preview-images").prepend(`\n <a class="preview-video-container" data-fancybox="gallery" href="#preview-video">\n <span>预告片</span>\n <img src="${videoCoverSrc}" class="video-cover" style="width: 150px; height: auto;" alt="">\n </a>\n `);
|
||||
$(".preview-video-container").on("click", (event => {
|
||||
utils.loopDetector((() => $(".fancybox-content #preview-video").length > 0), (async () => {
|
||||
await this.handleVideo();
|
||||
@@ -60,19 +61,52 @@ class PreviewVideoPlugin extends BasePlugin {
|
||||
clog.error("预加载dmm失败:", error);
|
||||
}
|
||||
}
|
||||
|
||||
async handleVideo() {
|
||||
if (await storageManager.getSetting("enableLoadPreviewVideo", YES) === NO) return;
|
||||
const $videoEl = $("#preview-video");
|
||||
if (!$videoEl.length) return;
|
||||
const $videoContainer = $videoEl.parent();
|
||||
$videoContainer.css("position", "relative");
|
||||
const videoEl = $videoEl[0], jhs_videoMuted = localStorage.getItem("jhs_videoMuted");
|
||||
jhs_videoMuted && (videoEl.muted = "yes" === jhs_videoMuted);
|
||||
const videoEl = $videoEl[0];
|
||||
let carNum2 = this.getPageInfo().carNum;
|
||||
|
||||
const jhs_videoMuted = localStorage.getItem("jhs_videoMuted");
|
||||
if (jhs_videoMuted) videoEl.muted = "yes" === jhs_videoMuted;
|
||||
videoEl.addEventListener("volumechange", (function() {
|
||||
localStorage.setItem("jhs_videoMuted", videoEl.muted ? "yes" : "no");
|
||||
}));
|
||||
|
||||
const CACHE_KEY = "jhs_dmm_video";
|
||||
let dmmErrorRetryCount = 0;
|
||||
const attachVideoErrorHandler = () => {
|
||||
videoEl.addEventListener("error", async function onVideoError(e) {
|
||||
if (dmmErrorRetryCount >= 2) return;
|
||||
dmmErrorRetryCount++;
|
||||
clog.warn(`预览视频加载失败(第${dmmErrorRetryCount}次),清除缓存后重试...`);
|
||||
const cachedData = localStorage.getItem(CACHE_KEY) ? JSON.parse(localStorage.getItem(CACHE_KEY)) : {};
|
||||
delete cachedData[carNum2];
|
||||
localStorage.setItem(CACHE_KEY, JSON.stringify(cachedData));
|
||||
const freshMap = await getDmmVideo(carNum2);
|
||||
if (!freshMap) {
|
||||
show.error("预览视频获取失败,请稍后重试");
|
||||
return;
|
||||
}
|
||||
const storedQuality = await storageManager.getSetting("videoQuality");
|
||||
const quality = selectDefaultQuality(Object.keys(freshMap), storedQuality);
|
||||
const freshUrl = freshMap[quality];
|
||||
const savedTime = videoEl.currentTime;
|
||||
$videoEl.attr("src", freshUrl);
|
||||
videoEl.load();
|
||||
if (savedTime > 0 && Number.isFinite(savedTime)) {
|
||||
videoEl.currentTime = savedTime;
|
||||
}
|
||||
videoEl.play();
|
||||
}, {once: false});
|
||||
};
|
||||
attachVideoErrorHandler();
|
||||
videoEl.play();
|
||||
let carNum2 = this.getPageInfo().carNum;
|
||||
|
||||
const dmmVideoMap = await getDmmVideo(carNum2);
|
||||
let $bottomToolbar = $("<div></div>").attr("id", "video-bottom-toolbar").css({
|
||||
display: "flex",
|
||||
@@ -122,7 +156,9 @@ class PreviewVideoPlugin extends BasePlugin {
|
||||
const currentTime = videoEl.currentTime;
|
||||
$videoEl.attr("src", videoSrc);
|
||||
videoEl.load();
|
||||
videoEl.currentTime = currentTime;
|
||||
if (currentTime > 0 && Number.isFinite(currentTime)) {
|
||||
videoEl.currentTime = currentTime;
|
||||
}
|
||||
await videoEl.play();
|
||||
$bottomToolbar.find(".video-control-btn").removeClass("active").css({
|
||||
"background-color": "#fff",
|
||||
|
||||
+20
-6
File diff suppressed because one or more lines are too long
@@ -6,8 +6,9 @@ const getDownPathList = async () => {
|
||||
return "object" == typeof res ? res.data : null;
|
||||
};
|
||||
|
||||
const searchFiles = async (search_value, offset = 0, limit = 30) => {
|
||||
const url = `https://webapi.115.com/files/search?search_value=${encodeURIComponent(search_value)}&offset=${offset}&limit=${limit}`;
|
||||
const searchFiles = async (search_value, offset = 0, limit = 30, params = {}) => {
|
||||
const queryParams = new URLSearchParams({search_value, offset, limit, ...params});
|
||||
const url = `https://webapi.115.com/files/search?${queryParams}`;
|
||||
return await gmHttp.get(url);
|
||||
};
|
||||
|
||||
|
||||
+207
-39
@@ -275,7 +275,13 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
|
||||
const $detail = $('\n <div class="jhs-match-detail jhs-115-match-detail" id="115-match-table">\n <table>\n <thead>\n <tr style="text-align: center">\n <th colspan="4">115匹配</th>\n </tr>\n <tr>\n <th>名称</th>\n <th>大小</th>\n <th>时间</th>\n <th>播放</th>\n </tr>\n </thead>\n <tbody>\n </tbody>\n </table>\n </div>\n '), $tbody = $detail.find("tbody");
|
||||
try {
|
||||
const carNum2 = this.getPageInfo().carNum;
|
||||
matchList || (matchList = await this.searchFiles(carNum2));
|
||||
if (!matchList) {
|
||||
const {results, usedPhase2} = await this._searchFilesWithPhase(carNum2);
|
||||
matchList = results;
|
||||
if (usedPhase2 && matchList.length > 0) {
|
||||
await this.autoRenameFiles(carNum2, matchList);
|
||||
}
|
||||
}
|
||||
await this.checkLoginStatus();
|
||||
if (this.loginStatus === _WangPan115MatchPlugin.LoginStatus.LOGGED_OUT) $tbody.append(`<tr><td colspan="4">\n <a class='jhs-match-no-login-btn a-info'\n data-keyword="${carNum2}"\n title="未登录115网盘">未登录</a>\n </td></tr>`); else if (matchList.length > 0) {
|
||||
const rowsHtml = matchList.map((match => `\n <tr>\n <td>${match.name}</td>\n <td>${this.formatSize(match.size)}</td>\n <td>${match.createTime}</td>\n <td>\n <a href="https://115vod.com/?pickcode=${match.videoId}&share_id=0"\n target="_blank"\n class="a-success"\n title="播放">播放</a>\n </td>\n </tr>\n `)).join("");
|
||||
@@ -393,43 +399,56 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
|
||||
async batchDeleteMarked(selectedItems, onProgress) {
|
||||
let deletedCount = 0;
|
||||
let failedCount = 0;
|
||||
const delay = ms => new Promise(resolve => setTimeout(resolve, ms));
|
||||
const randomDelay = () => new Promise(resolve => setTimeout(resolve, 1000 + Math.random() * 2000));
|
||||
for (let i = 0; i < selectedItems.length; i++) {
|
||||
const item = selectedItems[i];
|
||||
try {
|
||||
if (i > 0) {
|
||||
await randomDelay();
|
||||
}
|
||||
const matchList = await this.searchFiles(item.carNum);
|
||||
if (matchList.length === 0) {
|
||||
failedCount++;
|
||||
onProgress && onProgress(i + 1, selectedItems.length, item, "未匹配到115文件");
|
||||
continue;
|
||||
}
|
||||
const upperCarNum = item.carNum.toUpperCase();
|
||||
const safeMatchList = matchList.filter(m => m.name.toUpperCase().includes(upperCarNum));
|
||||
const {regex: safeRegex} = codeParse(item.carNum);
|
||||
const safeMatchList = matchList.filter(m => safeRegex.test(m.name));
|
||||
if (safeMatchList.length === 0) {
|
||||
failedCount++;
|
||||
onProgress && onProgress(i + 1, selectedItems.length, item, "无安全可删除目录");
|
||||
continue;
|
||||
}
|
||||
let itemDeleted = 0;
|
||||
const uniqueDirIds = [...new Set(safeMatchList.map((m) => m.dirId))];
|
||||
for (const dirId of uniqueDirIds) {
|
||||
try {
|
||||
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
|
||||
"fid[0]": dirId,
|
||||
ignore_warn: "1"
|
||||
});
|
||||
if (result && result.state) {
|
||||
const groupedByDir = {};
|
||||
for (const m of safeMatchList) {
|
||||
if (!groupedByDir[m.dirId]) groupedByDir[m.dirId] = [];
|
||||
groupedByDir[m.dirId].push(m);
|
||||
}
|
||||
const entries = Object.entries(groupedByDir);
|
||||
for (let g = 0; g < entries.length; g++) {
|
||||
if (g > 0) {
|
||||
await randomDelay();
|
||||
}
|
||||
const [dirId, items] = entries[g];
|
||||
if (items.length === 1) {
|
||||
const result = await this._deleteOne(dirId, false, i, selectedItems, item, onProgress);
|
||||
if (result) {
|
||||
itemDeleted++;
|
||||
deletedCount++;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(`删除目录失败: ${dirId}`, e);
|
||||
} else {
|
||||
for (let f = 0; f < items.length; f++) {
|
||||
if (f > 0) await randomDelay();
|
||||
const fileDeleted = await this._deleteOne(items[f].folderId, true, i, selectedItems, item, onProgress);
|
||||
if (fileDeleted) {
|
||||
itemDeleted++;
|
||||
deletedCount++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
onProgress && onProgress(i + 1, selectedItems.length, item, itemDeleted > 0 ? `已删除${itemDeleted}个` : "删除失败");
|
||||
if (i < selectedItems.length - 1) {
|
||||
await delay(800);
|
||||
}
|
||||
} catch (error) {
|
||||
failedCount++;
|
||||
onProgress && onProgress(i + 1, selectedItems.length, item, error.message || "出错");
|
||||
@@ -439,9 +458,63 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
|
||||
return {deletedCount, failedCount};
|
||||
}
|
||||
|
||||
isRateLimited(response) {
|
||||
if (!response) return false;
|
||||
if (response.state === false) {
|
||||
const msg = (response.error || response.msg || "").toLowerCase();
|
||||
if (msg.includes("频繁") || msg.includes("操作太频繁") || msg.includes("稍后再试")
|
||||
|| msg.includes("rate") || msg.includes("limit") || msg.includes("too many")) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (response.status === 403 || response.status === 429) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async _deleteOne(deleteId, isFile, batchIndex, selectedItems, item, onProgress) {
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
const label = isFile ? "文件" : "目录";
|
||||
while (retryCount <= maxRetries) {
|
||||
try {
|
||||
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
|
||||
"fid[0]": deleteId,
|
||||
ignore_warn: "1"
|
||||
});
|
||||
if (result && result.state) {
|
||||
return 1;
|
||||
} else {
|
||||
const errMsg = (result && result.error) || "未知错误";
|
||||
if (retryCount < maxRetries && this.isRateLimited(result)) {
|
||||
retryCount++;
|
||||
const backoff = (retryCount + 1) * 2000 + Math.random() * 1000;
|
||||
onProgress && onProgress(batchIndex + 1, selectedItems.length, item, `风控触发,${backoff / 1000}秒后重试(${retryCount}/${maxRetries})`);
|
||||
await new Promise(resolve => setTimeout(resolve, backoff));
|
||||
continue;
|
||||
}
|
||||
console.error(`删除${label}失败: ${deleteId}`, errMsg);
|
||||
return 0;
|
||||
}
|
||||
} catch (e) {
|
||||
if (retryCount < maxRetries && this.isRateLimited(e)) {
|
||||
retryCount++;
|
||||
const backoff = (retryCount + 1) * 2000 + Math.random() * 1000;
|
||||
onProgress && onProgress(batchIndex + 1, selectedItems.length, item, `风控触发,${backoff / 1000}秒后重试(${retryCount}/${maxRetries})`);
|
||||
await new Promise(resolve => setTimeout(resolve, backoff));
|
||||
continue;
|
||||
}
|
||||
console.error(`删除${label}失败: ${deleteId}`, e);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
showDeleteDialog(carNum2, matchList, afterDelete) {
|
||||
const upperCarNum = carNum2.toUpperCase();
|
||||
const safeMatchList = matchList.filter(m => m.name.toUpperCase().includes(upperCarNum));
|
||||
const {regex: safeRegex} = codeParse(carNum2);
|
||||
const safeMatchList = matchList.filter(m => safeRegex.test(m.name));
|
||||
const unsafeCount = matchList.length - safeMatchList.length;
|
||||
if (safeMatchList.length === 0) {
|
||||
show.error(`作品 "${carNum2}" 匹配到的 ${matchList.length} 个项目均不含番号,无法安全删除。请手动在115中删除。`);
|
||||
@@ -494,13 +567,32 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
|
||||
let loadObj = loading();
|
||||
let deletedCount = 0;
|
||||
try {
|
||||
const uniqueDirIds = [...new Set(selected.map((i) => safeMatchList[i].dirId))];
|
||||
for (const dirId of uniqueDirIds) {
|
||||
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
|
||||
"fid[0]": dirId,
|
||||
ignore_warn: "1"
|
||||
});
|
||||
result && result.state ? deletedCount++ : console.error(`删除目录失败: ${dirId}`, result);
|
||||
const selectedItems = selected.map((i) => safeMatchList[i]);
|
||||
const totalByDir = {};
|
||||
for (const m of safeMatchList) {
|
||||
totalByDir[m.dirId] = (totalByDir[m.dirId] || 0) + 1;
|
||||
}
|
||||
const groupedByDir = {};
|
||||
for (const item of selectedItems) {
|
||||
if (!groupedByDir[item.dirId]) groupedByDir[item.dirId] = [];
|
||||
groupedByDir[item.dirId].push(item);
|
||||
}
|
||||
for (const [dirId, items] of Object.entries(groupedByDir)) {
|
||||
if (items.length === totalByDir[dirId]) {
|
||||
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
|
||||
"fid[0]": dirId,
|
||||
ignore_warn: "1"
|
||||
});
|
||||
result && result.state ? deletedCount++ : console.error(`删除目录失败: ${dirId}`, result);
|
||||
} else {
|
||||
for (const item of items) {
|
||||
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
|
||||
"fid[0]": item.folderId,
|
||||
ignore_warn: "1"
|
||||
});
|
||||
result && result.state ? deletedCount++ : console.error(`删除文件失败: ${item.folderId}`, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
loadObj.close();
|
||||
show.ok(`作品 "${carNum2}" 已删除 ${deletedCount} 个文件夹`);
|
||||
@@ -530,20 +622,76 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
|
||||
window.open("https://115.com");
|
||||
}
|
||||
async searchFiles(carNum2) {
|
||||
return (await this._searchFilesWithPhase(carNum2)).results;
|
||||
}
|
||||
|
||||
async _searchFilesWithPhase(carNum2) {
|
||||
const {regex} = codeParse(carNum2);
|
||||
const searchKeyword = carNum2.toLowerCase().replace("fc2-", "");
|
||||
|
||||
const mapAndFilter = (rawData) => {
|
||||
return rawData.map((data => ({
|
||||
folderId: data.fid,
|
||||
dirId: data.cid,
|
||||
videoId: data.pc,
|
||||
name: data.n,
|
||||
createTime: utils.formatDate(new Date(1e3 * data.te)),
|
||||
size: data.s,
|
||||
isVideo: true
|
||||
}))).filter((x => x.folderId && regex.test(x.name)));
|
||||
};
|
||||
|
||||
var _a2;
|
||||
let searchKeyword = carNum2.toLowerCase().replace("fc2-", "");
|
||||
return (null == (_a2 = (await searchFiles(searchKeyword)).data) ? void 0 : _a2.map((data => ({
|
||||
folderId: data.fid,
|
||||
dirId: data.cid,
|
||||
videoId: data.pc,
|
||||
name: data.n,
|
||||
createTime: utils.formatDate(new Date(1e3 * data.te)),
|
||||
size: data.s,
|
||||
isVideo: [ ".mp4", ".avi", ".mov", ".mkv", ".flv", ".wmv" ].some((ext => {
|
||||
var _a3;
|
||||
return null == (_a3 = data.n) ? void 0 : _a3.toLowerCase().endsWith(ext);
|
||||
}))
|
||||
}))).filter((x => x.folderId && x.isVideo && x.name.toLowerCase().includes(searchKeyword)))) || [];
|
||||
const preciseData = (null == (_a2 = (await searchFiles(searchKeyword, 0, 30, {type: 4})).data) ? void 0 : _a2) || [];
|
||||
const preciseResults = mapAndFilter(preciseData);
|
||||
if (preciseResults.length > 0) return {results: preciseResults, usedPhase2: false};
|
||||
|
||||
const numberPart = extractNumberPart(carNum2);
|
||||
if (numberPart === searchKeyword) return {results: preciseResults, usedPhase2: false};
|
||||
|
||||
const fuzzyData = (null == (_a2 = (await searchFiles(numberPart, 0, 30, {type: 4})).data) ? void 0 : _a2) || [];
|
||||
return {results: mapAndFilter(fuzzyData), usedPhase2: true};
|
||||
}
|
||||
|
||||
buildRenameTarget(carNum, originalName) {
|
||||
const {regex} = codeParse(carNum);
|
||||
const match = regex.exec(originalName);
|
||||
if (!match) return null;
|
||||
const before = originalName.slice(0, match.index);
|
||||
const after = originalName.slice(match.index + match[0].length);
|
||||
return before + carNum + after;
|
||||
}
|
||||
|
||||
async autoRenameFiles(carNum, matchList) {
|
||||
try {
|
||||
const enableAutoRename = await storageManager.getSetting("enable115AutoRename", NO);
|
||||
if (enableAutoRename !== YES) return;
|
||||
const scope = await storageManager.getSetting("autoRenameScope", "vr_only");
|
||||
if (scope === "vr_only" && !carNum.toUpperCase().includes("VR")) return;
|
||||
const renameObj = {};
|
||||
matchList.forEach(match => {
|
||||
const newName = this.buildRenameTarget(carNum, match.name);
|
||||
if (newName && newName !== match.name) {
|
||||
renameObj[match.folderId] = newName;
|
||||
}
|
||||
});
|
||||
if (Object.keys(renameObj).length === 0) return;
|
||||
const formData = {};
|
||||
Object.entries(renameObj).forEach(([fid, newName]) => {
|
||||
formData[`files_new_name[${fid}]`] = newName;
|
||||
});
|
||||
const result = await gmHttp.postForm("https://webapi.115.com/files/batch_rename", formData);
|
||||
if (result && result.state) {
|
||||
Object.entries(renameObj).forEach(([fid, newName]) => {
|
||||
const match = matchList.find(m => m.folderId === fid);
|
||||
console.log(`[115重命名] ${match.name} → ${newName}`);
|
||||
});
|
||||
} else {
|
||||
console.warn("[115重命名] 重命名失败:", (result && result.error) || "未知错误");
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn("[115重命名] 重命名失败,跳过:", e.message);
|
||||
}
|
||||
}
|
||||
showMatchError($box2, carNum2, error) {
|
||||
$box2.find(".jhs-match-btn").replaceWith(`<a class='jhs-match-error-btn' data-keyword="${carNum2}" \n title="匹配失败,点击重试">匹配失败</a>`);
|
||||
@@ -608,5 +756,25 @@ __publicField(_WangPan115MatchPlugin, "LoginStatus", {
|
||||
|
||||
let WangPan115MatchPlugin = _WangPan115MatchPlugin;
|
||||
|
||||
const extractNumberPart = (carNum) => {
|
||||
let cleaned = carNum.replace(/^fc2[-_]?(ppv[-_])?/i, "");
|
||||
const match = cleaned.match(/(\d+)$/);
|
||||
return match ? match[1] : cleaned;
|
||||
};
|
||||
|
||||
const codeParse = (code) => {
|
||||
const codes = code.split(/-|_/);
|
||||
const sep = "\\s?(0|-|_){0,2}\\s?";
|
||||
|
||||
let pattern = codes.join(sep);
|
||||
if (/^fc2/i.test(code)) pattern = `${codes[0]}${sep}(ppv)?${sep}${codes.at(-1)}`;
|
||||
|
||||
return {
|
||||
code,
|
||||
codes,
|
||||
prefix: codes[0],
|
||||
regex: new RegExp(`(?<![a-z])${pattern}(?!\\d)`, "i"),
|
||||
};
|
||||
};
|
||||
|
||||
export { WangPan115Plugin, WangPan115MatchPlugin };
|
||||
+5
-6
@@ -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.9',
|
||||
author: 'xiebro,fireinrain',
|
||||
description: 'Jav-鉴黄师 增强脚本。列表页:作品状态标签、一键筛选、新作品检测、演员黑名单过滤;详情页:磁力链接高亮、DMM 多画质预览视频、标题翻译、整合字幕搜索(迅雷+SubTitleCat)并支持 115 直传、多源预览图(javfree/projectjav/javstore)含来源切换;数据:115 网盘目录匹配与多目录选择、云盘备份/恢复、跨 Tab 同步;其他:自动翻页、分类折叠、Top250、以图识图、热门榜单、评论查看、相关清单。支持 JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers',
|
||||
license: 'MIT',
|
||||
icon: 'https://www.google.com/s2/favicons?sz=64&domain=javdb.com',
|
||||
@@ -92,14 +92,13 @@ export default defineConfig({
|
||||
build: {
|
||||
fileName: 'jhs-enhance.user.js',
|
||||
externalGlobals: {
|
||||
'jquery': '$',
|
||||
'jquery': 'jQuery',
|
||||
'localforage': 'localforage',
|
||||
'toastify-js': 'Toastify',
|
||||
'blueimp-md5': 'md5',
|
||||
'tabulator-tables': 'Tabulator',
|
||||
'viewerjs': 'Viewer',
|
||||
'qrcodejs': 'QRCode',
|
||||
'layui-layer': 'layer',
|
||||
},
|
||||
},
|
||||
}),
|
||||
|
||||
Reference in New Issue
Block a user