重构拆分

This commit is contained in:
lzyme.dev@gmail.com
2026-08-29 16:41:55 +08:00
commit eab2e7a413
9 changed files with 13466 additions and 0 deletions
+10962
View File
File diff suppressed because one or more lines are too long
+228
View File
@@ -0,0 +1,228 @@
# JHS-enhance
> Jav-鉴黄师 油猴脚本 — 工程化重构版本
基于 `vite-plugin-monkey` 脚手架,将原 10,000+ 行单体脚本拆分为 49 个可维护的 ES Module 模块,支持本地开发热更新、一键构建。
---
## 功能概览
- **列表页增强**:作品状态标签(屏蔽/收藏/已下载/已观看)、一键打开待鉴定/已收藏作品、新作品检测、演员黑名单过滤
- **详情页增强**:磁力链接高亮、预览视频(DMM 多画质)、标题翻译、字幕搜索(SubTitleCat / 迅雷)、演员信息、相关清单
- **数据管理**:IndexedDB 本地存储、云盘备份/恢复(阿里云盘 / 115 网盘 / WebDAV)、跨 Tab 数据同步
- **自动化任务**:自动翻页、新作品检测、收藏演员同步、黑名单更新
- **UI 优化**:分类折叠、热榜 Top250、免 VIP 查看热播、以图识图、截图功能
- **多站点支持**:JavDB / JavBus / JavSee / SeeJav / FC2 / JavTrailers / SubTitleCat
---
## 项目结构
```
JHS-enhance/
├── src/
│ ├── main.js # 入口文件:全局初始化、插件注册、页面启动
│ ├── core/ # 基础设施层
│ │ ├── constants.js # 全局常量、CSS 样式、页面类型判断
│ │ ├── base-plugin.js # 插件基类(含 SVG 图标、页面信息解析)
│ │ ├── plugin-manager.js # 插件管理器(注册、CSS/插件生命周期)
│ │ ├── globals.js # 全局依赖挂载(jQuery、localforage 等)
│ │ ├── storage.js # IndexedDB 存储管理器(localforage 封装)
│ │ ├── utils.js # 工具函数集(DOM 操作、弹窗、快捷键等)
│ │ └── gmHttp.js # GM_xmlhttpRequest 封装(带重试/并发控制)
│ ├── api/ # API 层
│ │ ├── javdb.js # JavDB API 封装(签名、搜索、评论等)
│ │ └── dmm.js # DMM 预览视频获取(多画质)
│ └── plugins/ # 插件层(38 个功能插件)
│ ├── list-page.js # 列表页过滤与状态标签
│ ├── list-page-button.js # 列表页功能按钮(待鉴定/已收藏/新作品/黑名单)
│ ├── detail-page.js # 详情页数据加载
│ ├── detail-page-button.js# 详情页操作按钮
│ ├── setting.js # 设置面板(Tabulator 表格)
│ ├── translate.js # 标题翻译(Google 翻译 API)
│ ├── preview-video.js # DMM 预览视频
│ ├── highlight-magnet.js # 磁力链接高亮匹配
│ ├── auto-page.js # 自动翻页 / 云盘备份
│ ├── blacklist.js # 黑名单管理(演员/番号)
│ ├── favorite-actresses.js# 收藏演员同步
│ ├── new-video.js # 新作品检测
│ ├── task.js # 后台任务调度
│ ├── history.js # 浏览历史
│ ├── wangpan-115.js # 115 网盘匹配
│ ├── wangpan-115-task.js # 115 网盘任务处理
│ ├── aliyun-pan.js # 阿里云盘
│ ├── fc2.js # FC2 页面处理
│ ├── fc2-by123av.js # FC2 通过 123AV 搜索
│ ├── actress-info.js # 演员信息展示
│ ├── related.js # 相关清单
│ ├── review.js # 评论展示
│ ├── cover-button.js # 封面按钮
│ ├── fold-category.js # 分类折叠
│ ├── hit-show.js # 热播榜单
│ ├── top250.js # Top250 排行榜
│ ├── nav-bar.js # JavDB 导航栏增强
│ ├── bus-nav-bar.js # JavBus 导航栏增强
│ ├── bus-detail-page.js # JavBus 详情页
│ ├── bus-img.js # JavBus 图片处理
│ ├── bus-preview-video.js # JavBus 预览视频
│ ├── image-recognition.js # 以图识图
│ ├── screenshot.js # 截图功能
│ ├── magnet-hub.js # MagnetHub 磁力搜索
│ ├── other-site.js # 第三方站点链接
│ ├── jav-trailers.js # JavTrailers 站点
│ ├── subtitle-cat.js # SubTitleCat 字幕搜索
│ ├── filter-title-keyword.js # 标题关键词过滤
│ └── want-watched.js # 想看/已看视频列表
├── vite.config.mjs # Vite + vite-plugin-monkey 配置
├── package.json
└── JHS-origin.js # 原始单体脚本(参考)
```
---
## 技术栈
| 类别 | 技术 |
|------|------|
| 构建工具 | Vite 6 + vite-plugin-monkey 5 |
| 模块系统 | ES Module |
| DOM 操作 | jQuery 3.7 |
| 数据存储 | localforage (IndexedDB) |
| 表格组件 | Tabulator 6 |
| 弹窗组件 | layui-layer |
| Toast 通知 | Toastify |
| 图片查看 | ViewerJS |
| 二维码 | QRCodeJS |
| 哈希 | blueimp-md5 |
| 代码风格 | 纯 JavaScript(无 TypeScript) |
---
## 快速开始
### 安装依赖
```bash
npm install
```
### 开发模式
```bash
npm run dev
```
启动后会在终端输出一个本地代理服务器地址,将该地址填入 Tampermonkey 即可实现热更新开发。
### 生产构建
```bash
npm run build
```
构建产物输出到 `dist/jhs-enhance.user.js`,可直接安装到 Tampermonkey。
---
## 架构设计
### 三层架构
```
main.js (入口)
│
├── core/ 基础设施层
│ ├── 全局常量、CSS 样式
│ ├── 插件基类、插件管理器
│ ├── 存储、网络、工具函数
│ └── 全局依赖挂载
│
├── api/ API 层
│ ├── JavDB API(签名、搜索)
│ └── DMM 视频 API
│
└── plugins/ 插件层
├── 列表页插件(按站点分)
├── 详情页插件(按站点分)
├── 功能插件(翻译/黑名单/备份等)
└── 第三方站点插件
```
### 插件机制
每个插件继承 `BasePlugin`,实现以下生命周期方法:
- `getName()` — 返回插件唯一标识
- `initCss()` — 返回插件专属 CSS(可选)
- `handle()` — 插件主逻辑
插件通过 `PluginManager` 统一注册和管理,按站点类型(JavDB / JavBus)分两组注册,确保不同站点加载正确的插件。
### 页面类型判断
```
isDetailPage → 作品详情页
isListPage → 作品列表页
isFc2Page → FC2 搜索结果页
isSearchPage → 搜索页
```
插件在 `handle()` 中通过 `window.isDetailPage` 等全局变量判断当前页面类型,决定是否执行逻辑。
---
## 开发指南
### 添加新插件
1. 在 `src/plugins/` 下创建 `my-plugin.js`
2. 继承 `BasePlugin`,实现 `getName()` 和 `handle()`
3. 在 `src/main.js` 中 import 并注册:
```javascript
import { MyPlugin } from './plugins/my-plugin.js';
// 在对应站点分支中注册
pluginManager.register(MyPlugin);
```
### 访问全局服务
插件中可通过以下方式访问全局服务:
```javascript
await storageManager.getSetting('key'); // 存储
await storageManager.getCarList(); // 车辆列表
this.getBean('OtherPluginName'); // 获取其他插件实例
window.utils.q(event, '确认?', callback); // 确认弹窗
window.show.ok('操作成功'); // Toast 通知
```
### 样式注入
- **全局样式**:定义在 `core/constants.js` 的 `mainCss` 中,启动时自动注入
- **站点样式**:`javBusStyle` / `javdbStyle` 按站点条件注入
- **插件样式**:通过 `initCss()` 方法返回,由 PluginManager 统一注入
---
## 构建配置
`vite-plugin-monkey` 在 `vite.config.mjs` 中配置了:
- **match/include**:脚本匹配的 URL 模式
- **require**:CDN 外部依赖(jQuery、Tabulator 等)
- **connect**:GM_xmlhttpRequest 允许的跨域域名
- **grant**:GM_* API 权限声明
---
## 版本历史
- **v3.3.6** — 工程化重构版本,模块化拆分,支持 HMR 开发
---
## License
MIT
+251
View File
@@ -0,0 +1,251 @@
好的,让我先全面分析这个 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(基础设施层)** 开始,这是最独立、最不容易出错的模块。
+49
View File
@@ -0,0 +1,49 @@
const fs = require('fs');
const path = require('path');
const allConstants = [
'isJavBus', 'isJavDb', 'isSearchPage', 'currentHref',
'Status_RUNNING', 'Status_SUCCESS', 'Status_FAIL', 'Status_LOADING',
'Status_FILTER', 'Status_FAVORITE', 'Status_HAS_DOWN', 'Status_HAS_WATCH',
'NO', 'YES'
];
const pluginsDir = 'src/plugins';
const files = fs.readdirSync(pluginsDir).filter(f => f.endsWith('.js'));
let fixed = 0;
for (const file of files) {
const filePath = path.join(pluginsDir, file);
let content = fs.readFileSync(filePath, 'utf8');
const needed = allConstants.filter(c => new RegExp('\\b' + c + '\\b').test(content));
if (needed.length === 0) continue;
const importMatch = content.match(/import\s*\{([^}]+)\}\s*from\s*['"][^'"]*constants[^'"]*['"]/);
if (importMatch) {
const existing = importMatch[1].split(',').map(s => s.trim()).filter(Boolean);
const missing = needed.filter(c => !existing.includes(c));
if (missing.length > 0) {
const newImports = [...existing, ...missing].join(', ');
const newImportLine = `import { ${newImports} } from '../core/constants.js';`;
content = content.replace(importMatch[0], newImportLine);
fs.writeFileSync(filePath, content);
console.log(`Fixed: ${file} +${missing.join(', ')}`);
fixed++;
}
} else {
const newImportLine = `import { ${needed.join(', ')} } from '../core/constants.js';`;
const baseImportMatch = content.match(/import\s*\{[^}]*\}\s*from\s*['"][^'"]*base-plugin[^'"]*['"]/);
if (baseImportMatch) {
content = content.replace(baseImportMatch[0], baseImportMatch[0] + '\n' + newImportLine);
fs.writeFileSync(filePath, content);
console.log(`Fixed: ${file} +${needed.join(', ')} (new import)`);
fixed++;
}
}
}
console.log(`\nTotal fixed: ${fixed} files`);
+1628
View File
File diff suppressed because it is too large Load Diff
+26
View File
@@ -0,0 +1,26 @@
{
"name": "jhs-enhance",
"version": "3.3.6",
"description": "Jav-鉴黄师 油猴脚本 - 工程化版本",
"type": "module",
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview"
},
"license": "MIT",
"devDependencies": {
"vite": "^6.0.0",
"vite-plugin-monkey": "^5.0.0"
},
"dependencies": {
"blueimp-md5": "^2.19.0",
"jquery": "^4.0.0",
"layui-layer": "^1.0.9",
"localforage": "^1.10.0",
"qrcodejs": "^1.0.0",
"tabulator-tables": "^6.5.2",
"toastify-js": "^1.12.0",
"viewerjs": "^1.12.0"
}
}
+181
View File
@@ -0,0 +1,181 @@
import { qualityOptions } from '../core/constants.js';
const selectDefaultQuality = (dmmVideoQualityList, intendedDefault) => {
if (!dmmVideoQualityList || 0 === dmmVideoQualityList.length) return null;
const availableSet = new Set(dmmVideoQualityList);
if (availableSet.has(intendedDefault)) return intendedDefault;
const priorityOrder = qualityOptions.map((option => option.quality)).reverse();
for (const quality of priorityOrder) if (availableSet.has(quality)) return quality;
return dmmVideoQualityList[0];
}, CACHE_KEY = "jhs_dmm_video";
class DmmVideoFetcher {
constructor(carNum2, showErrorMessages = !0) {
this.carNum = carNum2;
this.showErrorMessages = showErrorMessages;
}
_checkCache() {
const cachedData = localStorage.getItem(CACHE_KEY) ? JSON.parse(localStorage.getItem(CACHE_KEY)) : {};
if (cachedData[this.carNum]) {
clog.debug("缓存中存在预览视频信息", cachedData[this.carNum]);
return cachedData[this.carNum];
}
return null;
}
_updateCache(videoMap) {
const cachedData = localStorage.getItem(CACHE_KEY) ? JSON.parse(localStorage.getItem(CACHE_KEY)) : {};
cachedData[this.carNum] = videoMap;
clog.debug("成功解析出预览视频并已缓存:", videoMap);
localStorage.setItem(CACHE_KEY, JSON.stringify(cachedData));
}
async _searchContentIds() {
const carNum2 = this.carNum, carNumNoHyphen = carNum2.replace(/-/g, ""), keywordAttempts = [ {
keyword: carNum2.replace("-", "00"),
name: "00-替换关键词"
}, {
keyword: carNum2,
name: "原始番号关键词"
}, {
keyword: carNumNoHyphen,
name: "无连字符关键词"
} ], carNumLower = carNum2.toLowerCase();
for (const attempt of keywordAttempts) {
const {keyword: keyword, name: name2} = attempt, currentTempCarNumLower = keyword.toLowerCase(), apiUrl2 = `https://api.dmm.com/affiliate/v3/ItemList?${new URLSearchParams({
api_id: "UrwskPfkqQ0DuVry2gYL",
affiliate_id: "10278-996",
output: "json",
site: "FANZA",
sort: "match",
keyword: keyword
}).toString()}`;
let response;
try {
response = await gmHttp.get(apiUrl2);
} catch (e) {
clog.error(`API 请求失败,跳过 ${name2}:`, e);
continue;
}
if (!response || !response.result || !response.result.result_count) {
clog.debug(`使用 ${name2} (${keyword}) 进行 API 搜索 返回无结果,尝试下一个关键词。`);
continue;
}
const newItems = [];
for (const item of response.result.items) {
if (newItems.length >= 2) break;
const contentId = item.content_id || "", makerProduct = item.maker_product || "";
if (contentId.includes(currentTempCarNumLower.replace("-", "")) || carNumLower === makerProduct.toLowerCase() || contentId.includes(carNumNoHyphen.toLowerCase())) {
newItems.push({
serviceCode: item.service_code,
floorCode: item.floor_code,
contentId: contentId,
pageUrl: item.URL
});
clog.debug(`[${name2}] cid|makerProduct 匹配成功:`, contentId, makerProduct);
}
}
if (newItems.length > 0) {
clog.debug(`--- 成功通过 ${name2} 找到 Content IDs ---`);
const $btn2 = $("#fanzaBtn");
let url = `https://www.dmm.co.jp/search/=/searchstr=${keyword}`, type = "single";
if (newItems.length > 1) {
$btn2.attr("href", url);
$btn2.append('<span class="site-tag" style="top:-15px">多结果</span>');
$btn2.css("backgroundColor", "#7bc73b");
type = "multiple";
} else {
url = newItems[0].pageUrl;
$btn2.attr("href", url);
$btn2.css("backgroundColor", "#7bc73b");
}
const dmmCacheKey = "jhs_other_site_dmm", dmmCacheData = localStorage.getItem(dmmCacheKey) ? JSON.parse(localStorage.getItem(dmmCacheKey)) : {};
dmmCacheData[this.carNum] = {
type: type,
url: url
};
localStorage.setItem(dmmCacheKey, JSON.stringify(dmmCacheData));
return newItems;
}
clog.debug(`[${name2}] API 返回结果数 ${response.result.result_count},但无精确匹配的 Content ID。`);
}
clog.warn("所有关键词尝试均未找到匹配的Content ID, 解析Dmm视频失败");
const $btn = $("#fanzaBtn");
$btn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`);
$btn.attr("title", "未查询到, 点击前往搜索页");
$btn.css("backgroundColor", "#de3333");
return null;
}
async _extractTrailerLinks({contentId: contentId, serviceCode: serviceCode, floorCode: floorCode}) {
const trailerPageUrl = `https://www.dmm.co.jp/service/digitalapi/-/html5_player/=/cid=${contentId}/mtype=AhRVShI_/service=${serviceCode}/floor=${floorCode}/mode=/`, htmlContent = await gmHttp.get(trailerPageUrl, null, {
"accept-language": "ja-JP,ja;q=0.9",
Cookie: "age_check_done=1"
});
if ("string" != typeof htmlContent) {
clog.error(htmlContent);
throw new Error("解析播放页内容失败, 非文本内容");
}
if (htmlContent.includes("このサービスはお住まいの地域からは")) throw new Error("节点不可用,请将DMM域名分流到日本ip");
const match = htmlContent.match(/const\s+args\s+=\s+(.*);/);
if (!match) throw new Error("未在脚本中找到 const args = ... 变量");
let bitrates;
try {
({bitrates: bitrates} = JSON.parse(match[1]));
} catch (e) {
throw new Error(`解析播放器脚本 JSON 失败: ${e.message}`);
}
const finalQualityMap = {}, qualityKeys = qualityOptions.map((o => o.quality)).join("|"), qualityNameRegex = new RegExp(`(${qualityKeys})\\.mp4$`);
if (!Array.isArray(bitrates)) {
clog.error("解析画质链接失败: bitrates 字段不是一个数组或不存在");
throw new Error("解析画质链接失败: bitrates 字段不是一个数组或不存在");
}
clog.debug("原始数据返回:", bitrates);
for (const item of bitrates) {
const url = null == item ? void 0 : item.src;
if (!url || "string" != typeof url || !url.endsWith(".mp4")) continue;
const qualityMatch = url.match(qualityNameRegex);
let qualityKey = "";
qualityMatch && qualityMatch[1] && (qualityKey = qualityMatch[1]);
qualityKey && !finalQualityMap[qualityKey] && (finalQualityMap[qualityKey] = url);
}
if (0 === Object.keys(finalQualityMap).length) throw new Error("未找到匹配要求的预览画质视频");
return finalQualityMap;
}
async fetchVideo() {
const cachedResult = this._checkCache();
if (cachedResult) return cachedResult;
let contentItems;
try {
const testCarNum = this.carNum.toLowerCase();
if (testCarNum.startsWith("heyzo") || /^(n\d+|\d+(-\d+)*)$/.test(testCarNum) || /^n\d+$/.test(testCarNum)) throw new Error("无码番号类型, 取消dmm解析");
if (this.carNum.includes("VR-")) throw new Error("VR类型, 取消dmm解析");
contentItems = await this._searchContentIds();
} catch (e) {
clog.error("DMM API 搜索失败:", e);
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 ];
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");
$btn.attr("href", `https://www.dmm.co.jp/search/=/searchstr=${this.carNum}`);
$btn.attr("title", "未查询到, 点击前往搜索页");
$btn.css("backgroundColor", "#de3333");
return null;
}
}
}
const getDmmVideo = async (carNum2, showErrorMessages = !0) => new DmmVideoFetcher(carNum2, showErrorMessages).fetchVideo();
export { DmmVideoFetcher, getDmmVideo, selectDefaultQuality };
+33
View File
@@ -0,0 +1,33 @@
import $ from 'jquery';
import localforage from 'localforage';
import Toastify from 'toastify-js';
import md5 from 'blueimp-md5';
import { TabulatorFull as Tabulator } from 'tabulator-tables';
import Viewer from 'viewerjs';
import QRCode from 'qrcodejs';
import { currentHref, isJavDb, isJavBus, isSearchPage, Status_RUNNING, Status_SUCCESS, Status_FAIL, Status_LOADING, Status_FILTER, Status_FAVORITE, Status_HAS_DOWN, Status_HAS_WATCH, NO, YES } from './constants.js';
window.$ = window.jQuery = $;
window.localforage = localforage;
window.Toastify = Toastify;
window.md5 = md5;
window.Tabulator = Tabulator;
window.Viewer = Viewer;
window.QRCode = QRCode;
window.currentHref = currentHref;
window.isJavDb = isJavDb;
window.isJavBus = isJavBus;
window.isSearchPage = isSearchPage;
window.Status_RUNNING = Status_RUNNING;
window.Status_SUCCESS = Status_SUCCESS;
window.Status_FAIL = Status_FAIL;
window.Status_LOADING = Status_LOADING;
window.Status_FILTER = Status_FILTER;
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';
+108
View File
@@ -0,0 +1,108 @@
import { defineConfig } from 'vite';
import monkey from 'vite-plugin-monkey';
export default defineConfig({
plugins: [
monkey({
entry: 'src/main.js',
userscript: {
name: 'JAV-JHS',
namespace: 'JAV-JHS',
version: '3.3.6-fix',
author: 'xie bro',
description: 'Jav-鉴黄师 收藏、屏蔽、标记已下载; 屏蔽标签、屏蔽演员、同步收藏演员、新作品检测; 免VIP查看热播、Top250排行榜、Fc2ppv、可查看所有评论信息、相关清单; 支持云盘备份; 以图识图; 字幕搜索; JavDb|JavBus',
license: 'MIT',
icon: 'https://www.google.com/s2/favicons?sz=64&domain=javdb.com',
match: [
'https://javdb.com/*',
'https://www.javbus.com/*',
],
include: [
'https://javdb*.com/*',
'https://*javbus*/*',
'https://*javsee*/*',
'https://*seejav*/*',
'https://javtrailers.com/*',
'https://subtitlecat.com/*',
'https://www.aliyundrive.com/*',
'https://www.alipan.com/*',
'https://115.com/*',
],
exclude: [
'https://*javbus*/forum/*',
'https://*javbus*/*actresses',
'https://*javsee*/forum/*',
'https://*javsee*/*actresses',
'https://*seejav*/forum/*',
'https://*seejav*/*actresses',
],
require: [
'https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js',
'https://cdn.jsdelivr.net/npm/jquery@3.7.1/dist/jquery.min.js',
'https://cdn.jsdelivr.net/npm/tabulator-tables@6.3.1/dist/js/tabulator.min.js',
'https://cdn.jsdelivr.net/npm/layui-layer@1.0.9/dist/layer.min.js',
'https://cdn.jsdelivr.net/npm/blueimp-md5@2.19.0/js/md5.min.js',
'https://cdn.jsdelivr.net/npm/toastify-js@1.12.0/src/toastify.min.js',
'https://cdn.jsdelivr.net/npm/localforage@1.10.0/dist/localforage.min.js',
'https://cdn.jsdelivr.net/npm/viewerjs@1.11.1/dist/viewer.min.js',
'https://cdn.jsdelivr.net/npm/qrcodejs@1.0.0/qrcode.min.js',
],
connect: [
'xunlei.com',
'geilijiasu.com',
'aliyundrive.com',
'aliyundrive.net',
'ja.wikipedia.org',
'beta.magnet.pics',
'jdforrepam.com',
'cc3001.dmm.co.jp',
'cc3001.dmm.com',
'www.dmm.co.jp',
'www.dmm.com',
'api.dmm.com',
'special.dmm.co.jp',
'adult.contents.fc2.com',
'fc2ppvdb.com',
'123av.com',
'u3c3.com',
'u9a9.com',
'btsow.lol',
'sukebei.nyaa.si',
'javstore.net',
'javbest.net',
'missav.live',
'jable.tv',
'www.av.gl',
'jav.rs',
'javtrailers.com',
'javdb.com',
'javbus.com',
'supjav.com',
'115.com',
'jianguoyun.com',
'127.0.0.1',
'*',
],
grant: [
'GM_xmlhttpRequest',
'GM_openInTab',
'unsafeWindow',
],
'run-at': 'document-idle',
},
build: {
fileName: 'jhs-enhance.user.js',
externalGlobals: {
'jquery': '$',
'localforage': 'localforage',
'toastify-js': 'Toastify',
'blueimp-md5': 'md5',
'tabulator-tables': 'Tabulator',
'viewerjs': 'Viewer',
'qrcodejs': 'QRCode',
'layui-layer': 'layer',
},
},
}),
],
});