10 Commits

Author SHA1 Message Date
lzyme.dev@gmail.com e74f2cbdc8 修复磁力搜索获取失败刷新参数丢失 2026-09-10 16:32:00 +08:00
lzyme.dev@gmail.com 8396e3825d UI调整 2026-09-09 14:58:50 +08:00
lzyme.dev@gmail.com e4792cd55f 修复javbus离线下载按钮风格 2026-09-09 14:39:40 +08:00
lzyme.dev@gmail.com 72ec9a0e83 修复javbus下显示异常 2026-09-05 10:19:09 +08:00
lzyme.dev@gmail.com 4339f1b80f update 2026-09-03 23:45:08 +08:00
lzyme.dev@gmail.com 20d199f53a bump version 2026-09-03 21:46:01 +08:00
lzyme.dev@gmail.com a9e88cda88 移除javxy的接口依赖 2026-09-03 21:45:01 +08:00
lzyme.dev@gmail.com 032dcf1fdc 移除javxy的接口依赖 2026-09-03 19:23:21 +08:00
lzyme.dev@gmail.com 38221bc78d update 2026-09-02 23:26:37 +08:00
lzyme.dev@gmail.com 4230176ce4 修复github action发布版本号问题 2026-09-01 18:54:13 +08:00
13 changed files with 319 additions and 579 deletions
+6 -2
View File
@@ -27,11 +27,15 @@ jobs:
- name: Build
run: npm run build
- name: Get version
id: version
run: echo "VERSION=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT
- name: Upload to Release
uses: softprops/action-gh-release@v2
with:
tag_name: v${{ github.run_number }}
name: Release v${{ github.run_number }}
tag_name: v${{ steps.version.outputs.VERSION }}
name: Release v${{ steps.version.outputs.VERSION }}
files: dist/jhs-enhance.user.js
draft: false
prerelease: false
+5 -5
View File
@@ -42,7 +42,7 @@
- **磁力排序与过滤(JavDB)**:按发布日期/文件大小/文件数量拖拽排序,勾选启用;高清/4K/字幕/无码多条件并集过滤,4K 磁力行高亮显示
- 磁力链接高亮匹配已收藏/已下载状态
- DMM 多画质预览视频
- **多源视频预览**:Javxy 聚合 API 独立按钮(番号/识别码右侧),多源 fallback(JavTrailers / JavDB 等),HLS 支持、播放进度记忆、画质切换
- **多源视频预览**:DMMRSC 聚合 API 独立按钮(番号/识别码右侧),HLS 支持、播放进度记忆、画质切换
- **标题翻译**:Google 翻译,支持开关多次切换不失效
- **字幕搜索**:合并迅雷 + SubTitleCat 为单一入口,支持 115 网盘直传、详情页预匹配目录、多目录选择上传、长目录名截断展示、字幕预览绿色字体
- **多源预览图**:javfree / projectjav / javstore 三源切换,来源按钮绿色高亮,sessionStorage 缓存,错误提示弹窗内显示
@@ -110,7 +110,7 @@ JHS-enhance/
│ │ ├── javdb.js # JavDB API 封装(签名算法、搜索、评论、磁力列表)
│ │ ├── dmm.js # DMM 预览视频获取(多画质、默认画质选择)
│ │ ├── dmm-graphql.js # DMM GraphQL API(备选视频源回退)
│ │ └── javxy.js # Javxy 聚合 API(视频多源解析、高清封面获取、签名生成)
│ │ └── dmmrsc.js # DMMRSC API(视频多源解析、高清封面获取)
│ ├── lib/ # 第三方补丁库
│ │ ├── gm-xhr-parallel.js # Tampermonkey MV3 并行请求修复(redirect: manual)
│ │ └── hls-runtime.js # HLS 视频播放运行时(hls.js 封装、进度记忆、画质切换)
@@ -153,7 +153,7 @@ JHS-enhance/
│ ├── magnet-hub.js # MagnetHub 磁力搜索(6 引擎聚合、质量标签、验车截图、115 离线)
│ ├── other-site.js # 第三方站点跳转链接
│ ├── jav-trailers.js # JavTrailers 站点处理
│ ├── javxy-preview-video.js# Javxy 多源视频预览(独立按钮、聚合 API、HLS 支持)
│ ├── javxy-preview-video.js# DMMRSC 多源视频预览(独立按钮、聚合 API、HLS 支持)
│ ├── subtitle-cat.js # SubTitleCat 字幕搜索(已整合,保留原文件)
│ ├── filter-title-keyword.js # 标题关键词过滤
│ └── want-watched.js # 想看/已看视频列表
@@ -318,11 +318,11 @@ window.show.ok('操作成功'); // Toast 通知
- 优化:`searchFiles` 增加 `type:4` 参数,搜索仅返回视频文件,避免目录占用 30 条槽位
- **v3.3.8**
- 新增:Javxy 多源视频预览独立按钮(番号/识别码右侧),基于 Javxy 聚合 API,支持 JavTrailers / JavDB 等多源回退
- 新增:DMMRSC 多源视频预览独立按钮(番号/识别码右侧),基于 DMMRSC 聚合 API
- 新增:HLS 视频播放运行时(hls.js 封装),支持 .m3u8 格式、播放进度记忆、画质切换
- 新增:DMM GraphQL API 模块,作为备选视频源回退方案
- 优化:`bus-preview-video.js` 和 `preview-video.js` 清理移植代码,保持原有播放逻辑不变
- 优化:`dmm.js` 回退至仅 DMM Affiliate API + GraphQL API 双通道,移除 Javxy 耦合
- 优化:`dmm.js` 回退至仅 DMM Affiliate API + GraphQL API 双通道
- **v3.3.7**
- 新增:JavDB 磁力排序与过滤(拖拽排序、多条件并集过滤、4K 高亮)
-251
View File
@@ -1,251 +0,0 @@
好的,让我先全面分析这个 11000 行的脚本。我会分段读取,理解它的整体结构和功能模块。
The user wants me to analyze the JHS-origin.js file (about 11,000 lines) and provide a detailed report on how to modularize it. Let me first read through the entire file to understand its structure, then provide a comprehensive refactoring plan.
Since the file is 10,962 lines, I need to read it in chunks. Let me start by reading several sections to understand the code structure.
Let me read the file in multiple chunks to understand:
1. The beginning (already read some)
2. Key sections throughout
3. The end
I'll read multiple sections in parallel.
Let me continue reading more sections to get a complete picture of the codebase.
Let me also read the end of the file to see the initialization/entry point.
Now I have a very comprehensive understanding of the codebase. Let me also read the very end to see the initialization.
Now I have a comprehensive understanding of the entire codebase. Let me write a detailed analysis report.
The code is approximately 10,962 lines, with a well-defined plugin architecture:
- PluginManager: manages plugins
- BasePlugin: base class for all plugins
- ~40 plugins extending BasePlugin
The code also contains:
- Utility classes (Utils, gmHttp, StorageManager, show, clog, etc.)
- CSS styles
- Constants and configuration
- API interfaces (JavDbApi, etc.)
- A plugin system with registration and execution
Let me now write a comprehensive refactoring report.
现在我已经完整分析了整个代码库。以下是详细的结构分析报告:
---
## 🔍 JHS-enhance 代码结构分析报告
### 📊 总体概况
| 指标 | 数据 |
|------|------|
| 总行数 | **10,962 行** |
| 核心类/插件 | **40+ 个** |
| 支持站点 | JavDB、JavBus、JavTrailers、SubTitleCat、阿里云盘、115网盘 |
| 外部依赖 | jQuery、Tabulator、layui-layer、MD5、Toastify、localForage、ViewerJS、QRCodeJS |
---
### 🏗️ 现有架构
代码已经有一个**不错的插件架构**,只是全部写在一个文件里:
```
入口(最后100行)
├── 初始化 pluginManager
├── 检测当前页面类型(isDetailPage / isListPage / isFc2Page)
├── 按站点注册插件
├── pluginManager.processCss() → 加载所有插件CSS
└── pluginManager.processPlugins() → 执行所有插件
```
插件基类 `BasePlugin` 提供了:
- `getName()` — 插件名
- `initCss()` — 返回CSS字符串
- `handle()` — 主逻辑入口
- `getPageInfo()` — 获取当前页面信息
- `getBean(name)` — 获取其他插件实例
---
### 📦 代码功能模块划分
我将 11000 行代码按功能分为以下 **8 大模块**,建议拆分为独立文件:
---
#### 模块一:🛠️ 基础设施层(~1500 行)
**建议目录:`src/core/`**
| 文件 | 内容 | 说明 |
|------|------|------|
| `utils.js` | `Utils` 类(~300行) | 通用工具函数:下载、滚动、剪贴板、URL解析、排序等 |
| `gmHttp.js` | `gmHttp` 类(~200行) | 基于 `GM_xmlhttpRequest` 的 HTTP 封装 |
| `storage.js` | `StorageManager` 类(~500行) | IndexedDB 存储管理:收藏、屏蔽、设置、演员等 |
| `show.js` | `show` / `clog` 对象(~100行) | Toast 消息提示、控制台日志 |
| `constants.js` | 所有常量(~200行) | 状态枚举、画质选项、翻译映射表、标签样式等 |
| `styles.js` | CSS 样式(~300行) | JavDB/JavBus 站点样式、全局样式、滚动条样式 |
---
#### 模块二:🔌 插件系统(~200 行)
**建议目录:`src/core/`**
| 文件 | 内容 | 说明 |
|------|------|------|
| `plugin-manager.js` | `PluginManager` 类 | 插件注册、CSS加载、插件执行 |
| `base-plugin.js` | `BasePlugin` 类 | 插件基类,提供 `getName()`、`handle()`、`initCss()` 等 |
---
#### 模块三:🌐 API 接口层(~200 行)
**建议目录:`src/api/`**
| 文件 | 内容 | 说明 |
|------|------|------|
| `javdb-api.js` | `javDbApi` 对象 | JavDB API:获取评论、搜索电影、电影详情、磁力链接 |
---
#### 模块四:📋 列表页插件(~2500 行)
**建议目录:`src/plugins/list/`**
| 文件 | 类名 | 功能 |
|------|------|------|
| `list-page.js` | `ListPagePlugin` | 列表页核心:状态标签渲染、屏蔽过滤、演员屏蔽、排序 |
| `list-page-button.js` | `ListPageButtonPlugin` | 列表页工具栏按钮:一键屏蔽、收藏、随机打开等 |
| `auto-page.js` | `AutoPagePlugin` | 自动翻页 |
| `fold-category.js` | `FoldCategoryPlugin` | 折叠/展开分类标签区域 |
| `cover-button.js` | `CoverButtonPlugin` | 封面图上的操作按钮(屏蔽、收藏等) |
| `history.js` | `HistoryPlugin` | 鉴定记录面板 |
| `filter-title-keyword.js` | `FilterTitleKeywordPlugin` | 标题关键词屏蔽 |
| `blacklist.js` | `BlacklistPlugin` | 黑名单管理(屏蔽演员/标签) |
| `hit-show.js` | `HitShowPlugin` | 热播榜 |
| `top250.js` | `TOP250Plugin` | TOP250 排行榜 |
---
#### 模块五:🎬 详情页插件(~2000 行)
**建议目录:`src/plugins/detail/`**
| 文件 | 类名 | 功能 |
|------|------|------|
| `detail-page.js` | `DetailPagePlugin` | 详情页核心:FC2/普通影片信息展示 |
| `detail-page-button.js` | `DetailPageButtonPlugin` | 详情页按钮:屏蔽、收藏、已下载、已观看、磁力搜索、字幕 |
| `bus-detail-page.js` | `BusDetailPagePlugin` | JavBus 详情页 |
| `review.js` | `ReviewPlugin` | 评论查看 |
| `related.js` | `RelatedPlugin` | 相关清单 |
| `highlight-magnet.js` | `HighlightMagnetPlugin` | 磁力链接高亮过滤 |
| `translate.js` | `TranslatePlugin` | 标题翻译(日→中) |
| `want-watched.js` | `WantAndWatchedVideosPlugin` | 想看/已看视频 |
---
#### 模块六:🎥 视频与媒体插件(~1500 行)
**建议目录:`src/plugins/media/`**
| 文件 | 类名 | 功能 |
|------|------|------|
| `preview-video.js` | `PreviewVideoPlugin` | JavDB 预览视频播放 |
| `bus-preview-video.js` | `BusPreviewVideoPlugin` | JavBus 预览视频播放 |
| `screen-shot.js` | `ScreenShotPlugin` | 缩略图加载 |
| `bus-img.js` | `BusImgPlugin` | JavBus 图片处理 |
| `image-recognition.js` | `ImageRecognitionPlugin` | 以图识图 |
---
#### 模块七:🔗 外部站点与工具插件(~2000 行)
**建议目录:`src/plugins/external/`**
| 文件 | 类名 | 功能 |
|------|------|------|
| `magnet-hub.js` | `MagnetHubPlugin` | 多站点磁力搜索聚合 |
| `other-site.js` | `OtherSitePlugin` | 其他站点入口(JavStore、MissAV等) |
| `fc2.js` | `Fc2Plugin` | FC2 影片页面 |
| `fc2-123av.js` | `Fc2By123AvPlugin` | 123AV 的 FC2 页面 |
| `wangpan-115.js` | `WangPan115Plugin` / `WangPan115TaskPlugin` / `WangPan115MatchPlugin` | 115网盘离线下载 |
| `aliyun-pan.js` | `AliyunPanPlugin` | 阿里云盘 |
| `jav-trailers.js` | `JavTrailersPlugin` | JavTrailers 站点 |
| `subtitle-cat.js` | `SubTitleCatPlugin` | SubTitleCat 字幕搜索 |
---
#### 模块八:👤 演员与任务插件(~2000 行)
**建议目录:`src/plugins/actress/`**
| 文件 | 类名 | 功能 |
|------|------|------|
| `actress-info.js` | `ActressInfoPlugin` | 演员信息面板 |
| `favorite-actresses.js` | `FavoriteActressesPlugin` | 收藏演员管理 |
| `new-video.js` | `NewVideoPlugin` | 新作品检测 |
| `task.js` | `TaskPlugin` | 定时任务:检测黑名单、同步演员、新作品检测 |
| `nav-bar.js` | `NavBarPlugin` / `BusNavBarPlugin` | 导航栏增强 |
| `setting.js` | `SettingPlugin` | 设置面板 |
---
### 🎯 建议的项目目录结构
```
JHS-enhance/
├── src/
│ ├── main.js # 入口:初始化 + 插件注册
│ ├── core/
│ │ ├── utils.js # 通用工具函数
│ │ ├── gmHttp.js # GM_xmlhttpRequest 封装
│ │ ├── storage.js # IndexedDB 存储管理
│ │ ├── show.js # Toast 消息提示
│ │ ├── constants.js # 所有常量
│ │ ├── styles.js # CSS 样式
│ │ ├── plugin-manager.js # 插件管理器
│ │ └── base-plugin.js # 插件基类
│ ├── api/
│ │ └── javdb-api.js # JavDB API 接口
│ ├── plugins/
│ │ ├── list/ # 列表页插件 (10个)
│ │ ├── detail/ # 详情页插件 (8个)
│ │ ├── media/ # 视频/媒体插件 (5个)
│ │ ├── external/ # 外部站点插件 (8个)
│ │ └── actress/ # 演员/任务插件 (6个)
│ └── styles/
│ └── ... (可选,如果CSS拆出来)
├── vite.config.mjs
├── package.json
└── JHS-origin.js # 保留原始文件作为参考
```
---
### ⚠️ 拆分时的注意事项
1. **全局变量依赖**:当前代码大量使用 `unsafeWindow.xxx = window.xxx`,拆分为 ES Module 后需要改为 `export`/`import`,同时通过 `window` 暴露给油猴全局作用域
2. **插件间依赖**:插件通过 `this.getBean(name)` 互相调用,拆分后需确保 `PluginManager` 先初始化
3. **CDN 外部依赖**:jQuery、Tabulator 等通过 `@require` 加载,在模块中直接使用 `$`、`Tabulator` 等全局变量即可
4. **CSS 注入**:`insertStyle()` 和 `initCss()` 需要 `document.head` 可用后才能执行
5. **循环依赖**:注意 `TaskPlugin` ↔ `BlacklistPlugin`、`ListPagePlugin` ↔ `BlacklistPlugin` 等之间的双向引用
---
### 🚀 建议的实施顺序
| 优先级 | 步骤 | 预计工作量 |
|--------|------|-----------|
| P0 | 拆分 `core/` 基础设施(utils, gmHttp, storage, show, constants, styles) | 1-2小时 |
| P1 | 拆分插件系统(plugin-manager, base-plugin)和入口 `main.js` | 30分钟 |
| P2 | 拆分 API 层(javdb-api) | 15分钟 |
| P3 | 分批拆分插件,每批 3-5 个,逐个验证构建 | 3-5小时 |
| P4 | 清理全局变量,统一为 ES Module 导入导出 | 1小时 |
---
需要我开始执行拆分吗?建议从 **P0(基础设施层)** 开始,这是最独立、最不容易出错的模块。
+142 -245
View File
@@ -1,7 +1,7 @@
// ==UserScript==
// @name JHS-enhance
// @namespace JHS-enhance
// @version 3.3.11
// @version 3.3.14
// @author xiebro,fireinrain
// @description Jav-鉴黄师 增强脚本。列表页:作品状态标签、一键筛选、新作品检测、演员黑名单过滤;详情页:磁力链接高亮、DMM 多画质预览视频、标题翻译、整合字幕搜索(迅雷+SubTitleCat)并支持 115 直传、多源预览图(javfree/projectjav/javstore)含来源切换;数据:115 网盘目录匹配与多目录选择、云盘备份/恢复、跨 Tab 同步;其他:自动翻页、分类折叠、Top250、以图识图、热门榜单、评论查看、相关清单。支持 JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers
// @license MIT
@@ -284,6 +284,16 @@ System.register("./__entry.js", ['jquery', 'localforage', 'toastify-js', 'blueim
quality: "mmb",
text: "中画质 (432p)",
canSelect: true
}, {
id: "video-dm",
quality: "dm",
text: "低画质 (1000kbps)",
canSelect: true
}, {
id: "video-dmb",
quality: "dmb",
text: "中画质 (1500kbps)",
canSelect: true
}, {
id: "video-mhb",
quality: "mhb",
@@ -792,6 +802,23 @@ System.register("./__entry.js", ['jquery', 'localforage', 'toastify-js', 'blueim
color: #f40 !important;
}
#magnets-content .item[data-rank] {
position: relative !important;
}
#magnets-content .item[data-rank] .buttons {
position: absolute !important;
bottom: 0;
right: 0;
margin: 0 !important;
display: flex;
gap: 4px;
align-items: center;
}
#magnets-content .item[data-rank] .name {
display: block;
padding-right: 260px;
}
.msf-overlay {
position: fixed;
inset: 0;
@@ -10267,191 +10294,56 @@ ${err.stack}` : "");
videoEl._hls = null;
}
};
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 ^ 37)).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 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 apiUrl2 = `https://${endpoint.host}/trailers/${encodeURIComponent(query)}?${params}`;
clog.debug("Javxy 请求 API", {query, apiUrl: apiUrl2, endpoint: endpoint.label});
let r;
try {
r = await gmHttp.get(apiUrl2, null, {
"Accept": "application/json,text/plain,*/*",
"X-Javxy-Token": JAVXY_TOKEN
}, {timeout: 8e3});
} 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 == null ? void 0 : r.trailer) || "").trim();
if (!trailerUrl) {
clog.debug("Javxy 无 trailer 字段", {endpoint: endpoint.label, keys: Object.keys(r || {})});
return null;
}
const qualityMap = (r == null ? void 0 : r.qualities) && typeof r.qualities === "object" ? r.qualities : {};
const quality = (r == null ? void 0 : r.quality) && qualityMap[r.quality] ? r.quality : selectHighestQuality(qualityMap);
const sourceBase = JAVXY_SOURCE_LABELS[r == null ? void 0 : r.source] || `Javxy | ${(r == null ? void 0 : r.source) || "dmm"}`;
const directUrl = qualityMap[quality] || trailerUrl;
clog.debug("Javxy 返回结果", {
endpoint: endpoint.label,
source: r == null ? void 0 : r.source,
quality,
qualities: Object.keys(qualityMap),
url: directUrl
});
const dmmQualityMap = javxyQualityMapToDmmFormat(qualityMap);
return {
url: directUrl,
source: sourceBase,
type: String((r == null ? void 0 : r.type) || "").trim() || "video",
qualities: qualityMap,
quality,
directUrl,
code: id,
rawCode,
javxySource: String((r == null ? void 0 : r.source) || "").trim(),
requiresJP: Boolean(r == null ? void 0 : r.requiresJP),
urls: Array.isArray(r == null ? void 0 : 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((source2) => normalizeJavxySource(source2)).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 == null ? void 0 : result.dmmQualityMap) && Object.keys(result.dmmQualityMap).length > 0) {
return result.dmmQualityMap;
}
if (result == null ? void 0 : result.url) {
const qualityMap = {};
qualityMap["720p"] = result.url;
return qualityMap;
}
return null;
};
const getJavxyCover = async (code) => {
const COVER_API_URL = [77, 81, 81, 85, 86, 31, 10, 10, 68, 85, 76, 8, 65, 72, 72, 87, 86, 70, 11, 20, 22, 20, 17, 22, 22, 11, 93, 92, 95, 10, 68, 85, 76, 10, 70, 74, 83, 64, 87].map((v) => String.fromCharCode(v ^ 37)).join("");
const TRAILER_API_URL = [77, 81, 81, 85, 86, 31, 10, 10, 68, 85, 76, 8, 65, 72, 72, 87, 86, 70, 11, 20, 22, 20, 17, 22, 22, 11, 93, 92, 95, 10, 68, 85, 76, 10, 81, 87, 68, 76, 73, 64, 87].map((v) => String.fromCharCode(v ^ 37)).join("");
const BASE_URL = [77, 81, 81, 85, 86, 31, 10, 10, 68, 85, 76, 8, 65, 72, 72, 87, 86, 70, 11, 20, 22, 20, 17, 22, 22, 11, 93, 92, 95].map((v) => String.fromCharCode(v ^ 37)).join("");
const COVER_API_TOKEN = [18, 28, 19, 19, 65, 65, 70, 28, 64, 29, 23, 21, 17, 29, 67, 70, 18, 64, 21, 22, 64, 16, 65, 29, 22, 19, 16, 28, 16, 17, 64, 29].map((v) => String.fromCharCode(v ^ 37)).join("");
const getCover = async (code) => {
var _a;
const query = String(code || "").trim();
if (!query) return null;
for (const endpoint of JAVXY_ENDPOINTS) {
const params = new URLSearchParams({client: "laosiji-new"});
const apiUrl2 = `https://${endpoint.host}/covers/${encodeURIComponent(query)}?${params}`;
let r;
try {
r = await gmHttp.get(apiUrl2, null, {
"Accept": "application/json,text/plain,*/*",
"X-Javxy-Token": JAVXY_TOKEN
}, {timeout: 15e3});
} catch (e) {
continue;
}
if (!r) continue;
if ((r == null ? void 0 : r.found) && (r.url || r.cover || r.highCover)) return r;
try {
const r = await gmHttp.get(`${COVER_API_URL}/${encodeURIComponent(query)}`, null, {
"Authorization": `Bearer ${COVER_API_TOKEN}`
}, {timeout: 15e3});
if ((_a = r == null ? void 0 : r.cover) == null ? void 0 : _a.large) {
return r.cover.large;
}
return null;
} catch (e) {
clog.error("获取封面失败:", e.message);
return null;
}
return null;
};
class JavxyPreviewVideoPlugin extends BasePlugin {
const getTrailer = async (code) => {
const query = String(code || "").trim();
if (!query) return null;
try {
const r = await gmHttp.get(`${TRAILER_API_URL}/${encodeURIComponent(query)}`, null, {
"Authorization": `Bearer ${COVER_API_TOKEN}`
}, {timeout: 15e3});
if ((r == null ? void 0 : r.trailers) && Array.isArray(r.trailers) && r.trailers.length > 0) {
const qualityMap = {};
r.trailers.forEach((item) => {
if (item.proxy && item.quality) {
qualityMap[item.quality] = `${BASE_URL}${item.proxy}`;
}
});
if (Object.keys(qualityMap).length > 0) {
clog.log("预告片播放地址:", JSON.stringify(qualityMap));
return qualityMap;
}
}
return null;
} catch (e) {
clog.error("获取预告片失败:", e.message);
return null;
}
};
class JavopenPreviewVideoPlugin extends BasePlugin {
getName() {
return "JavxyPreviewVideoPlugin";
return "JavopenPreviewVideoPlugin";
}
_attachVideoSrc(videoEl, src) {
if (!videoEl || !src) return;
@@ -10461,20 +10353,13 @@ ${err.stack}` : "");
if (HlsClass) {
attachHlsToVideo(videoEl, src).catch((err) => {
clog.error("视频预览 HLS 加载失败:", err);
videoEl.src = src;
videoEl.load && videoEl.load();
});
} else {
loadHlsLibrary().then((HlsClass2) => {
if (videoEl && videoEl.isConnected && HlsClass2) {
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();
}
});
}
@@ -10493,38 +10378,38 @@ ${err.stack}` : "");
}
async initCss() {
return `
.jhs-javxy-video-modal {
.jhs-javopen-video-modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.95); z-index: 12345700;
display: flex; justify-content: center; align-items: center;
opacity: 0; visibility: hidden; transition: opacity 0.2s;
}
.jhs-javxy-video-modal.is-open { opacity: 1; visibility: visible; }
.jhs-javxy-video-modal-inner {
.jhs-javopen-video-modal.is-open { opacity: 1; visibility: visible; }
.jhs-javopen-video-modal-inner {
display: flex; flex-direction: column; align-items: center;
gap: 12px; max-width: 90vw; max-height: 90vh;
}
.jhs-javxy-video-wrapper {
.jhs-javopen-video-wrapper {
width: 80vw; max-height: 80vh; aspect-ratio: 16/9;
background: #000; position: relative;
}
.jhs-javxy-video-wrapper video {
.jhs-javopen-video-wrapper video {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}
.jhs-javxy-quality-bar {
.jhs-javopen-quality-bar {
display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;
}
.jhs-javxy-quality-btn {
.jhs-javopen-quality-btn {
min-width: 60px; padding: 5px 10px; font-size: 13px;
background: rgba(255,255,255,0.2); color: #fff;
border: 1px solid rgba(255,255,255,0.5); border-radius: 4px;
cursor: pointer; transition: background 0.2s;
}
.jhs-javxy-quality-btn:hover { background: rgba(255,255,255,0.4); }
.jhs-javxy-quality-btn.active {
.jhs-javopen-quality-btn:hover { background: rgba(255,255,255,0.4); }
.jhs-javopen-quality-btn.active {
background: #1890ff; border-color: #096dd9; font-weight: bold;
}
.jhs-javxy-loading {
.jhs-javopen-loading {
color: #fff; font-size: 16px; text-align: center;
}
`;
@@ -10533,9 +10418,10 @@ ${err.stack}` : "");
if (!window.isDetailPage) return;
const carNum2 = this.getPageInfo().carNum;
if (!carNum2) return;
this._addJavxyButton(carNum2);
this._addJavopenButton(carNum2);
}
_addJavxyButton(carNum2) {
_addJavopenButton(carNum2) {
let $target = null;
if (isJavDb$1) {
$target = $('a[title="複製番號"]');
@@ -10563,30 +10449,31 @@ ${err.stack}` : "");
e.stopPropagation();
$btn.css("pointer-events", "none").find("span").text("...");
try {
await this._openJavxyPlayer(carNum2);
await this._openJavopenPlayer(carNum2);
} finally {
$btn.css("pointer-events", "").find("span").text("视频预览");
}
});
$target.after($btn);
}
async _openJavxyPlayer(carNum2) {
if ($("#jhs-javxy-video-modal").length === 0) {
async _openJavopenPlayer(carNum2) {
if ($("#jhs-javopen-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 id="jhs-javopen-video-modal" class="jhs-javopen-video-modal">
<div class="jhs-javopen-video-modal-inner">
<div class="jhs-javopen-video-wrapper">
<video id="jhs-javopen-video" controls playsinline></video>
</div>
<div class="jhs-javxy-quality-bar"></div>
<div class="jhs-javopen-quality-bar"></div>
</div>
</div>
`);
const $modal2 = $("#jhs-javxy-video-modal");
const $modal2 = $("#jhs-javopen-video-modal");
$modal2.on("click", (e) => {
if (e.target.id === "jhs-javxy-video-modal") {
if (e.target.id === "jhs-javopen-video-modal") {
$modal2.removeClass("is-open");
const videoEl2 = document.getElementById("jhs-javxy-video");
const videoEl2 = document.getElementById("jhs-javopen-video");
if (videoEl2) {
destroyHls(videoEl2);
videoEl2.pause();
@@ -10596,7 +10483,7 @@ ${err.stack}` : "");
$(document).on("keydown", (e) => {
if (e.key === "Escape" && $modal2.hasClass("is-open")) {
$modal2.removeClass("is-open");
const videoEl2 = document.getElementById("jhs-javxy-video");
const videoEl2 = document.getElementById("jhs-javopen-video");
if (videoEl2) {
destroyHls(videoEl2);
videoEl2.pause();
@@ -10604,30 +10491,30 @@ ${err.stack}` : "");
}
});
}
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>');
const $modal = $("#jhs-javopen-video-modal");
const $wrapper = $modal.find(".jhs-javopen-video-wrapper");
const $qualityBar = $modal.find(".jhs-javopen-quality-bar");
$wrapper.html('<div class="jhs-javopen-loading">视频预览加载中...</div>');
$qualityBar.empty();
$modal.addClass("is-open");
let videoMap = null;
try {
videoMap = await getJavxyVideoUrls(carNum2);
videoMap = await getTrailer(carNum2);
} catch (err) {
$wrapper.html(`<div class="jhs-javxy-loading">视频预览请求失败: ${err.message}</div>`);
$wrapper.html(`<div class="jhs-javopen-loading">视频预览请求失败: ${err.message}</div>`);
return;
}
if (!videoMap || Object.keys(videoMap).length === 0) {
$wrapper.html('<div class="jhs-javxy-loading">未找到可用的视频源</div>');
$wrapper.html('<div class="jhs-javopen-loading">未找到可用的视频源</div>');
return;
}
const qualityList = Object.keys(videoMap);
const defaultQuality = this._selectDefaultQuality(qualityList, "720p");
const defaultUrl = videoMap[defaultQuality];
$wrapper.html(`<video id="jhs-javxy-video" controls playsinline>
$wrapper.html(`<video id="jhs-javopen-video" controls playsinline>
<source src="${defaultUrl}" />
</video>`);
const videoEl = document.getElementById("jhs-javxy-video");
const videoEl = document.getElementById("jhs-javopen-video");
if (!videoEl) return;
if (isM3U8Url(defaultUrl)) {
this._attachVideoSrc(videoEl, defaultUrl);
@@ -10636,12 +10523,12 @@ ${err.stack}` : "");
qualityOptions.forEach((option) => {
const url = videoMap[option.quality];
if (url) {
buttonsHtml += `<button class="jhs-javxy-quality-btn${option.quality === defaultQuality ? " active" : ""}"
buttonsHtml += `<button class="jhs-javopen-quality-btn${option.quality === defaultQuality ? " active" : ""}"
data-quality="${option.quality}" data-video-src="${url}">${option.text}</button>`;
}
});
$qualityBar.html(buttonsHtml);
$qualityBar.off("click").on("click", ".jhs-javxy-quality-btn", (e) => {
$qualityBar.off("click").on("click", ".jhs-javopen-quality-btn", (e) => {
const $btn = $(e.currentTarget);
if ($btn.hasClass("active")) return;
const src = $btn.attr("data-video-src");
@@ -10654,7 +10541,7 @@ ${err.stack}` : "");
}
videoEl.play().catch(() => {
});
$qualityBar.find(".jhs-javxy-quality-btn").removeClass("active");
$qualityBar.find(".jhs-javopen-quality-btn").removeClass("active");
$btn.addClass("active");
});
videoEl.play().catch((e) => console.warn("视频预览播放失败:", e));
@@ -11647,6 +11534,8 @@ ${err.stack}` : "");
}
searchEngine($container, $sortSelect, engine, keyword) {
this.currentEngine = engine;
this.currentKeyword = keyword;
$container.html(`<div class="magnet-loading">正在从 ${engine.name} 搜索 "${keyword}"...</div>`);
const cacheKey = `${engine.name}_${keyword}`;
sessionStorage.getItem(cacheKey);
@@ -11664,11 +11553,11 @@ ${err.stack}` : "");
$sortSelect.prop("disabled", results.length === 0);
this.displayResults($container, results, engine.name);
} catch (e) {
$container.html(`<div class="magnet-error">解析 ${engine.name} 结果失败: ${e.message}</div>`);
this.showError($container, `解析 ${engine.name} 结果失败: ${e.message}`);
}
},
onerror: (error) => {
$container.html(`<div class="magnet-error">从 ${engine.name} 获取数据失败: ${error.statusText}</div>`);
this.showError($container, `从 ${engine.name} 获取数据失败: ${error.statusText}`);
}
}));
}
@@ -11677,6 +11566,15 @@ ${err.stack}` : "");
}
}));
}
showError($container, message) {
$container.html(`<div class="magnet-error">${message} <a id="retrySearch" href="#">🔄 刷新</a></div>`);
const $select = $container.closest(".magnet-container").find(".magnet-sort-select");
$container.find("#retrySearch").on("click", ((e) => {
e.preventDefault();
this.searchEngine($container, $select, this.currentEngine, this.currentKeyword);
}));
}
displayResults($container, results, engineName) {
$container.empty();
const sortMode = localStorage.getItem("jhs_magnetSort") || "size";
@@ -12071,11 +11969,11 @@ ${err.stack}` : "");
$sortSelect.prop("disabled", results.length === 0);
_this.displayResults($container, results, engine.name);
} catch (e) {
$container.html(`<div class="magnet-error">解析 ${engine.name} 结果失败: ${e.message}</div>`);
_this.showError($container, `解析 ${engine.name} 结果失败: ${e.message}`);
}
},
onerror: (error) => {
$container.html(`<div class="magnet-error">从 ${engine.name} 获取数据失败: ${error.statusText}</div>`);
_this.showError($container, `从 ${engine.name} 获取数据失败: ${error.statusText}`);
}
});
}
@@ -12625,12 +12523,12 @@ ${err.stack}` : "");
if ($downloadBtn.length) {
$downloadBtn.css("margin-right", parseInt($downloadBtn.css("margin-right")) + 3 + "px");
}
$("<button>").text("验车").addClass("button is-info is-small").click(((event) => {
$("<button>").text("验车").addClass("button is-small").attr("style", "background-color:#2b6cb0 !important;color:#fff !important;border:none !important;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click(((event) => {
event.stopPropagation();
event.preventDefault();
this.getBean("MagnetHubPlugin").checkWhatslink(magnet);
})).appendTo($copyBtn.parent());
$("<button>").text("115离线下载").addClass("button is-info is-small").click((async (event) => {
$("<button>").text("115离线下载").addClass("button is-small").attr("style", "background-color:#4a90e2 !important;color:#fff !important;border:none !important;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click((async (event) => {
event.stopPropagation();
event.preventDefault();
let loadObj = loading();
@@ -12649,11 +12547,18 @@ ${err.stack}` : "");
}));
}
async bus115Down() {
const $headerRow = $("#magnet-table tr").first();
$("#magnet-table tr").each(((i, row) => {
if (row === $headerRow[0]) return;
const magnetLink = $(row).find("td:nth-child(1) a").attr("href");
if (magnetLink && magnetLink.includes("magnet:")) {
const actionCell = $("<td>").addClass("action-cell");
$("<button>").text("115离线下载").addClass("button is-info is-small").click((async (event) => {
$("<button>").text("验车").addClass("button is-small").attr("style", "background-color:#2b6cb0 !important;color:#fff !important;border:none !important;margin-right:4px;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click(((event) => {
event.stopPropagation();
event.preventDefault();
this.getBean("MagnetHubPlugin").checkWhatslink(magnetLink);
})).appendTo(actionCell);
$("<button>").text("115离线下载").addClass("button is-small").attr("style", "background-color:#4a90e2 !important;color:#fff !important;border:none !important;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click((async (event) => {
event.stopPropagation();
event.preventDefault();
let loadObj = loading();
@@ -12665,16 +12570,13 @@ ${err.stack}` : "");
} finally {
loadObj.close();
}
})).appendTo(actionCell);
$("<button>").text("验车").addClass("button is-info is-small").click(((event) => {
event.stopPropagation();
event.preventDefault();
this.getBean("MagnetHubPlugin").checkWhatslink(magnetLink);
})).appendTo(actionCell);
$(row).append(actionCell);
}
}));
$("#magnet-table tbody").length > 0 && $("#magnet-table tbody tr").append($("<td>").text("操作"));
if ($headerRow.find("td, th").last().text().trim() !== "操作") {
$headerRow.append($("<td>").text("操作"));
}
}
async getSavePathId(nyName) {
let savePath115 = await storageManager.getSetting("savePath115", "云下载");
@@ -15720,14 +15622,9 @@ ${err.stack}` : "");
const $btn = $("#downloadCoverBtn");
$btn.addClass("is-loading").prop("disabled", true);
try {
const coverData = await getJavxyCover(carNum2);
if (!coverData) {
show.error("未找到高清封面");
return;
}
const coverUrl = coverData.url || coverData.highCover || coverData.cover;
const coverUrl = await getCover(carNum2);
if (!coverUrl) {
show.error("封面链接为空");
show.error("未找到高清封面");
return;
}
const fileName = `${carNum2}-cover.jpg`;
@@ -18514,7 +18411,7 @@ ${err.stack}` : "");
pluginManager2.register(DetailPageButtonPlugin);
pluginManager2.register(HighlightMagnetPlugin);
pluginManager2.register(PreviewVideoPlugin);
pluginManager2.register(JavxyPreviewVideoPlugin);
pluginManager2.register(JavopenPreviewVideoPlugin);
pluginManager2.register(FilterTitleKeywordPlugin);
pluginManager2.register(ActressInfoPlugin);
pluginManager2.register(OtherSitePlugin);
@@ -18545,7 +18442,7 @@ ${err.stack}` : "");
pluginManager2.register(FilterTitleKeywordPlugin);
pluginManager2.register(HighlightMagnetPlugin);
pluginManager2.register(BusPreviewVideoPlugin);
pluginManager2.register(JavxyPreviewVideoPlugin);
pluginManager2.register(JavopenPreviewVideoPlugin);
pluginManager2.register(MagnetHubPlugin);
pluginManager2.register(ScreenShotPlugin);
pluginManager2.register(OtherSitePlugin);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "jhs-enhance",
"version": "3.3.11",
"version": "3.3.15",
"description": "Jav-鉴黄师 油猴脚本 - 工程化版本",
"type": "module",
"scripts": {
+61
View File
@@ -0,0 +1,61 @@
const COVER_API_URL = [77, 81, 81, 85, 86, 31, 10, 10, 68, 85, 76, 8, 65, 72, 72, 87, 86, 70, 11, 20, 22, 20, 17, 22, 22, 11, 93, 92, 95, 10, 68, 85, 76, 10, 70, 74, 83, 64, 87]
.map(v => String.fromCharCode(v ^ 0x25)).join('');
const TRAILER_API_URL = [77, 81, 81, 85, 86, 31, 10, 10, 68, 85, 76, 8, 65, 72, 72, 87, 86, 70, 11, 20, 22, 20, 17, 22, 22, 11, 93, 92, 95, 10, 68, 85, 76, 10, 81, 87, 68, 76, 73, 64, 87]
.map(v => String.fromCharCode(v ^ 0x25)).join('');
const BASE_URL = [77, 81, 81, 85, 86, 31, 10, 10, 68, 85, 76, 8, 65, 72, 72, 87, 86, 70, 11, 20, 22, 20, 17, 22, 22, 11, 93, 92, 95]
.map(v => String.fromCharCode(v ^ 0x25)).join('');
const COVER_API_TOKEN = [18, 28, 19, 19, 65, 65, 70, 28, 64, 29, 23, 21, 17, 29, 67, 70, 18, 64, 21, 22, 64, 16, 65, 29, 22, 19, 16, 28, 16, 17, 64, 29]
.map(v => String.fromCharCode(v ^ 0x25)).join('');
const getCover = async (code) => {
const query = String(code || '').trim();
if (!query) return null;
try {
const r = await gmHttp.get(`${COVER_API_URL}/${encodeURIComponent(query)}`, null, {
'Authorization': `Bearer ${COVER_API_TOKEN}`,
}, {timeout: 15000});
if (r?.cover?.large) {
//clog.log('高清封面地址:', r.cover.large);
return r.cover.large;
}
return null;
} catch (e) {
clog.error('获取封面失败:', e.message);
return null;
}
};
const getTrailer = async (code) => {
const query = String(code || '').trim();
if (!query) return null;
try {
const r = await gmHttp.get(`${TRAILER_API_URL}/${encodeURIComponent(query)}`, null, {
'Authorization': `Bearer ${COVER_API_TOKEN}`,
}, {timeout: 15000});
if (r?.trailers && Array.isArray(r.trailers) && r.trailers.length > 0) {
const qualityMap = {};
r.trailers.forEach((item) => {
if (item.proxy && item.quality) {
qualityMap[item.quality] = `${BASE_URL}${item.proxy}`;
}
});
if (Object.keys(qualityMap).length > 0) {
clog.log('预告片播放地址:', JSON.stringify(qualityMap));
return qualityMap;
}
}
return null;
} catch (e) {
clog.error('获取预告片失败:', e.message);
return null;
}
};
export {getCover, getTrailer};
+27
View File
@@ -63,6 +63,16 @@ const currentHref = window.location.href,
quality: "mmb",
text: "中画质 (432p)",
canSelect: !0
}, {
id: "video-dm",
quality: "dm",
text: "低画质 (1000kbps)",
canSelect: !0
}, {
id: "video-dmb",
quality: "dmb",
text: "中画质 (1500kbps)",
canSelect: !0
}, {
id: "video-mhb",
quality: "mhb",
@@ -577,6 +587,23 @@ const mainCss = `
color: #f40 !important;
}
#magnets-content .item[data-rank] {
position: relative !important;
}
#magnets-content .item[data-rank] .buttons {
position: absolute !important;
bottom: 0;
right: 0;
margin: 0 !important;
display: flex;
gap: 4px;
align-items: center;
}
#magnets-content .item[data-rank] .name {
display: block;
padding-right: 260px;
}
.msf-overlay {
position: fixed;
inset: 0;
+3 -3
View File
@@ -29,7 +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 {JavopenPreviewVideoPlugin} from './plugins/javopen-preview-video.js';
import { ImageRecognitionPlugin } from './plugins/image-recognition.js';
import { BusNavBarPlugin } from './plugins/bus-nav-bar.js';
import { RelatedPlugin } from './plugins/related.js';
@@ -879,7 +879,7 @@ const pluginManager = function() {
pluginManager2.register(DetailPageButtonPlugin);
pluginManager2.register(HighlightMagnetPlugin);
pluginManager2.register(PreviewVideoPlugin);
pluginManager2.register(JavxyPreviewVideoPlugin);
pluginManager2.register(JavopenPreviewVideoPlugin);
pluginManager2.register(FilterTitleKeywordPlugin);
pluginManager2.register(ActressInfoPlugin);
pluginManager2.register(OtherSitePlugin);
@@ -910,7 +910,7 @@ const pluginManager = function() {
pluginManager2.register(FilterTitleKeywordPlugin);
pluginManager2.register(HighlightMagnetPlugin);
pluginManager2.register(BusPreviewVideoPlugin);
pluginManager2.register(JavxyPreviewVideoPlugin);
pluginManager2.register(JavopenPreviewVideoPlugin);
pluginManager2.register(MagnetHubPlugin);
pluginManager2.register(ScreenShotPlugin);
pluginManager2.register(OtherSitePlugin);
+3 -8
View File
@@ -13,7 +13,7 @@ import {
} from '../core/constants.js';
import { HotkeyManager } from './preview-video.js';
import {GM_xmlhttpRequest} from 'vite-plugin-monkey/dist/client';
import {getJavxyCover} from '../api/javxy.js';
import {getCover} from '../api/dmmrsc.js';
class DetailPageButtonPlugin extends BasePlugin {
getName() {
@@ -108,14 +108,9 @@ class DetailPageButtonPlugin extends BasePlugin {
const $btn = $("#downloadCoverBtn");
$btn.addClass("is-loading").prop("disabled", true);
try {
const coverData = await getJavxyCover(carNum2);
if (!coverData) {
show.error("未找到高清封面");
return;
}
const coverUrl = coverData.url || coverData.highCover || coverData.cover;
const coverUrl = await getCover(carNum2);
if (!coverUrl) {
show.error("封面链接为空");
show.error("未找到高清封面");
return;
}
const fileName = `${carNum2}-cover.jpg`;
@@ -1,11 +1,11 @@
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';
import {getTrailer} from '../api/dmmrsc.js';
class JavxyPreviewVideoPlugin extends BasePlugin {
class JavopenPreviewVideoPlugin extends BasePlugin {
getName() {
return "JavxyPreviewVideoPlugin";
return "JavopenPreviewVideoPlugin";
}
_attachVideoSrc(videoEl, src) {
@@ -17,20 +17,13 @@ class JavxyPreviewVideoPlugin extends BasePlugin {
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();
}
});
}
@@ -51,38 +44,38 @@ class JavxyPreviewVideoPlugin extends BasePlugin {
async initCss() {
return `
.jhs-javxy-video-modal {
.jhs-javopen-video-modal {
position: fixed; top: 0; left: 0; width: 100%; height: 100%;
background: rgba(0,0,0,0.95); z-index: 12345700;
display: flex; justify-content: center; align-items: center;
opacity: 0; visibility: hidden; transition: opacity 0.2s;
}
.jhs-javxy-video-modal.is-open { opacity: 1; visibility: visible; }
.jhs-javxy-video-modal-inner {
.jhs-javopen-video-modal.is-open { opacity: 1; visibility: visible; }
.jhs-javopen-video-modal-inner {
display: flex; flex-direction: column; align-items: center;
gap: 12px; max-width: 90vw; max-height: 90vh;
}
.jhs-javxy-video-wrapper {
.jhs-javopen-video-wrapper {
width: 80vw; max-height: 80vh; aspect-ratio: 16/9;
background: #000; position: relative;
}
.jhs-javxy-video-wrapper video {
.jhs-javopen-video-wrapper video {
position: absolute; top: 0; left: 0; width: 100%; height: 100%;
}
.jhs-javxy-quality-bar {
.jhs-javopen-quality-bar {
display: flex; gap: 8px; flex-wrap: wrap; justify-content: center;
}
.jhs-javxy-quality-btn {
.jhs-javopen-quality-btn {
min-width: 60px; padding: 5px 10px; font-size: 13px;
background: rgba(255,255,255,0.2); color: #fff;
border: 1px solid rgba(255,255,255,0.5); border-radius: 4px;
cursor: pointer; transition: background 0.2s;
}
.jhs-javxy-quality-btn:hover { background: rgba(255,255,255,0.4); }
.jhs-javxy-quality-btn.active {
.jhs-javopen-quality-btn:hover { background: rgba(255,255,255,0.4); }
.jhs-javopen-quality-btn.active {
background: #1890ff; border-color: #096dd9; font-weight: bold;
}
.jhs-javxy-loading {
.jhs-javopen-loading {
color: #fff; font-size: 16px; text-align: center;
}
`;
@@ -94,10 +87,10 @@ class JavxyPreviewVideoPlugin extends BasePlugin {
const carNum = this.getPageInfo().carNum;
if (!carNum) return;
this._addJavxyButton(carNum);
this._addJavopenButton(carNum);
}
_addJavxyButton(carNum) {
_addJavopenButton(carNum) {
let $target = null;
if (isJavDb) {
@@ -128,7 +121,7 @@ class JavxyPreviewVideoPlugin extends BasePlugin {
e.stopPropagation();
$btn.css("pointer-events", "none").find("span").text("...");
try {
await this._openJavxyPlayer(carNum);
await this._openJavopenPlayer(carNum);
} finally {
$btn.css("pointer-events", "").find("span").text("视频预览");
}
@@ -136,23 +129,23 @@ class JavxyPreviewVideoPlugin extends BasePlugin {
$target.after($btn);
}
async _openJavxyPlayer(carNum) {
if ($("#jhs-javxy-video-modal").length === 0) {
async _openJavopenPlayer(carNum) {
if ($("#jhs-javopen-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 id="jhs-javopen-video-modal" class="jhs-javopen-video-modal">
<div class="jhs-javopen-video-modal-inner">
<div class="jhs-javopen-video-wrapper">
<video id="jhs-javopen-video" controls playsinline></video>
</div>
<div class="jhs-javxy-quality-bar"></div>
<div class="jhs-javopen-quality-bar"></div>
</div>
</div>
`);
const $modal = $("#jhs-javxy-video-modal");
const $modal = $("#jhs-javopen-video-modal");
$modal.on("click", (e) => {
if (e.target.id === "jhs-javxy-video-modal") {
if (e.target.id === "jhs-javopen-video-modal") {
$modal.removeClass("is-open");
const videoEl = document.getElementById("jhs-javxy-video");
const videoEl = document.getElementById("jhs-javopen-video");
if (videoEl) {
destroyHls(videoEl);
videoEl.pause();
@@ -162,7 +155,7 @@ class JavxyPreviewVideoPlugin extends BasePlugin {
$(document).on("keydown", (e) => {
if (e.key === "Escape" && $modal.hasClass("is-open")) {
$modal.removeClass("is-open");
const videoEl = document.getElementById("jhs-javxy-video");
const videoEl = document.getElementById("jhs-javopen-video");
if (videoEl) {
destroyHls(videoEl);
videoEl.pause();
@@ -171,23 +164,23 @@ class JavxyPreviewVideoPlugin extends BasePlugin {
});
}
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>');
const $modal = $("#jhs-javopen-video-modal");
const $wrapper = $modal.find(".jhs-javopen-video-wrapper");
const $qualityBar = $modal.find(".jhs-javopen-quality-bar");
$wrapper.html('<div class="jhs-javopen-loading">视频预览加载中...</div>');
$qualityBar.empty();
$modal.addClass("is-open");
let videoMap = null;
try {
videoMap = await getJavxyVideoUrls(carNum);
videoMap = await getTrailer(carNum);
} catch (err) {
$wrapper.html(`<div class="jhs-javxy-loading">视频预览请求失败: ${err.message}</div>`);
$wrapper.html(`<div class="jhs-javopen-loading">视频预览请求失败: ${err.message}</div>`);
return;
}
if (!videoMap || Object.keys(videoMap).length === 0) {
$wrapper.html('<div class="jhs-javxy-loading">未找到可用的视频源</div>');
$wrapper.html('<div class="jhs-javopen-loading">未找到可用的视频源</div>');
return;
}
@@ -195,10 +188,10 @@ class JavxyPreviewVideoPlugin extends BasePlugin {
const defaultQuality = this._selectDefaultQuality(qualityList, '720p');
const defaultUrl = videoMap[defaultQuality];
$wrapper.html(`<video id="jhs-javxy-video" controls playsinline>
$wrapper.html(`<video id="jhs-javopen-video" controls playsinline>
<source src="${defaultUrl}" />
</video>`);
const videoEl = document.getElementById("jhs-javxy-video");
const videoEl = document.getElementById("jhs-javopen-video");
if (!videoEl) return;
if (isM3U8Url(defaultUrl)) {
@@ -209,13 +202,13 @@ class JavxyPreviewVideoPlugin extends BasePlugin {
qualityOptions.forEach((option) => {
const url = videoMap[option.quality];
if (url) {
buttonsHtml += `<button class="jhs-javxy-quality-btn${option.quality === defaultQuality ? ' active' : ''}"
buttonsHtml += `<button class="jhs-javopen-quality-btn${option.quality === defaultQuality ? ' active' : ''}"
data-quality="${option.quality}" data-video-src="${url}">${option.text}</button>`;
}
});
$qualityBar.html(buttonsHtml);
$qualityBar.off("click").on("click", ".jhs-javxy-quality-btn", (e) => {
$qualityBar.off("click").on("click", ".jhs-javopen-quality-btn", (e) => {
const $btn = $(e.currentTarget);
if ($btn.hasClass("active")) return;
const src = $btn.attr("data-video-src");
@@ -228,7 +221,7 @@ class JavxyPreviewVideoPlugin extends BasePlugin {
}
videoEl.play().catch(() => {
});
$qualityBar.find(".jhs-javxy-quality-btn").removeClass("active");
$qualityBar.find(".jhs-javopen-quality-btn").removeClass("active");
$btn.addClass("active");
});
@@ -236,4 +229,4 @@ class JavxyPreviewVideoPlugin extends BasePlugin {
}
}
export {JavxyPreviewVideoPlugin};
export {JavopenPreviewVideoPlugin};
+15 -4
View File
@@ -580,6 +580,8 @@ class MagnetHubPlugin extends BasePlugin {
}
searchEngine($container, $sortSelect, engine, keyword) {
this.currentEngine = engine;
this.currentKeyword = keyword;
$container.html(`<div class="magnet-loading">正在从 ${engine.name} 搜索 "${keyword}"...</div>`);
const cacheKey = `${engine.name}_${keyword}`;
sessionStorage.getItem(cacheKey);
@@ -597,11 +599,11 @@ class MagnetHubPlugin extends BasePlugin {
$sortSelect.prop("disabled", results.length === 0);
this.displayResults($container, results, engine.name);
} catch (e) {
$container.html(`<div class="magnet-error">解析 ${engine.name} 结果失败: ${e.message}</div>`);
this.showError($container, `解析 ${engine.name} 结果失败: ${e.message}`);
}
},
onerror: error => {
$container.html(`<div class="magnet-error">从 ${engine.name} 获取数据失败: ${error.statusText}</div>`);
this.showError($container, `从 ${engine.name} 获取数据失败: ${error.statusText}`);
}
}));
}
@@ -611,6 +613,15 @@ class MagnetHubPlugin extends BasePlugin {
}));
}
showError($container, message) {
$container.html(`<div class="magnet-error">${message} <a id="retrySearch" href="#">🔄 刷新</a></div>`);
const $select = $container.closest(".magnet-container").find(".magnet-sort-select");
$container.find("#retrySearch").on("click", (e => {
e.preventDefault();
this.searchEngine($container, $select, this.currentEngine, this.currentKeyword);
}));
}
displayResults($container, results, engineName) {
$container.empty();
const sortMode = localStorage.getItem("jhs_magnetSort") || "size";
@@ -1000,11 +1011,11 @@ class MagnetHubPlugin extends BasePlugin {
$sortSelect.prop("disabled", results.length === 0);
_this.displayResults($container, results, engine.name);
} catch (e) {
$container.html(`<div class="magnet-error">解析 ${engine.name} 结果失败: ${e.message}</div>`);
_this.showError($container, `解析 ${engine.name} 结果失败: ${e.message}`);
}
},
onerror: error => {
$container.html(`<div class="magnet-error">从 ${engine.name} 获取数据失败: ${error.statusText}</div>`);
_this.showError($container, `从 ${engine.name} 获取数据失败: ${error.statusText}`);
}
});
}
+13 -10
View File
@@ -20,18 +20,17 @@ class WangPan115TaskPlugin extends BasePlugin {
$(".buttons button[data-clipboard-text*='magnet:']").each(((i, el) => {
const $copyBtn = $(el);
const magnet = $copyBtn.attr("data-clipboard-text");
//对齐按钮间距
$copyBtn.css("margin-right", (parseInt($copyBtn.css("margin-right")) - 4) + "px");
const $downloadBtn = $copyBtn.siblings("a.button, button").first();
if ($downloadBtn.length) {
$downloadBtn.css("margin-right", (parseInt($downloadBtn.css("margin-right")) + 3) + "px");
}
$("<button>").text("验车").addClass("button is-info is-small").click((event => {
$("<button>").text("验车").addClass("button is-small").attr("style", "background-color:#2b6cb0 !important;color:#fff !important;border:none !important;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click((event => {
event.stopPropagation();
event.preventDefault();
this.getBean("MagnetHubPlugin").checkWhatslink(magnet);
})).appendTo($copyBtn.parent());
$("<button>").text("115离线下载").addClass("button is-info is-small").click((async event => {
$("<button>").text("115离线下载").addClass("button is-small").attr("style", "background-color:#4a90e2 !important;color:#fff !important;border:none !important;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click((async event => {
event.stopPropagation();
event.preventDefault();
let loadObj = loading();
@@ -50,11 +49,18 @@ class WangPan115TaskPlugin extends BasePlugin {
}));
}
async bus115Down() {
const $headerRow = $("#magnet-table tr").first();
$("#magnet-table tr").each(((i, row) => {
if (row === $headerRow[0]) return;
const magnetLink = $(row).find("td:nth-child(1) a").attr("href");
if (magnetLink && magnetLink.includes("magnet:")) {
const actionCell = $("<td>").addClass("action-cell");
$("<button>").text("115离线下载").addClass("button is-info is-small").click((async event => {
$("<button>").text("验车").addClass("button is-small").attr("style", "background-color:#2b6cb0 !important;color:#fff !important;border:none !important;margin-right:4px;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click((event => {
event.stopPropagation();
event.preventDefault();
this.getBean("MagnetHubPlugin").checkWhatslink(magnetLink);
})).appendTo(actionCell);
$("<button>").text("115离线下载").addClass("button is-small").attr("style", "background-color:#4a90e2 !important;color:#fff !important;border:none !important;cursor:pointer;border-radius:4px;font-size:12px;padding:2px 8px;").click((async event => {
event.stopPropagation();
event.preventDefault();
let loadObj = loading();
@@ -67,15 +73,12 @@ class WangPan115TaskPlugin extends BasePlugin {
loadObj.close();
}
})).appendTo(actionCell);
$("<button>").text("验车").addClass("button is-info is-small").click((event => {
event.stopPropagation();
event.preventDefault();
this.getBean("MagnetHubPlugin").checkWhatslink(magnetLink);
})).appendTo(actionCell);
$(row).append(actionCell);
}
}));
$("#magnet-table tbody").length > 0 && $("#magnet-table tbody tr").append($("<td>").text("操作"));
if ($headerRow.find("td, th").last().text().trim() !== "操作") {
$headerRow.append($("<td>").text("操作"));
}
}
async getSavePathId(nyName) {
let savePath115 = await storageManager.getSetting("savePath115", "云下载");
+3 -3
View File
@@ -6,9 +6,9 @@ export default defineConfig({
monkey({
entry: 'src/main.js',
userscript: {
name: 'JHS-enhance',
namespace: 'JHS-enhance',
version: '3.3.11',
name: 'JHS-enhance',
namespace: 'JHS-enhance',
version: '3.3.15',
author: 'xiebro,fireinrain',
description: 'Jav-鉴黄师 增强脚本。列表页:作品状态标签、一键筛选、新作品检测、演员黑名单过滤;详情页:磁力链接高亮、DMM 多画质预览视频、标题翻译、整合字幕搜索(迅雷+SubTitleCat)并支持 115 直传、多源预览图(javfree/projectjav/javstore)含来源切换;数据:115 网盘目录匹配与多目录选择、云盘备份/恢复、跨 Tab 同步;其他:自动翻页、分类折叠、Top250、以图识图、热门榜单、评论查看、相关清单。支持 JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers',
license: 'MIT',