mirror of
https://github.com/fireinrain/JHS-enhance.git
synced 2026-09-28 07:36:38 +08:00
Compare commits
14 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ce59d82ed2 | |||
| 37cf4e1183 | |||
| 8829af0fc3 | |||
| 9db06fba3b | |||
| 06b2497ee1 | |||
| 50581be5a6 | |||
| e92991c365 | |||
| 03700906e5 | |||
| e61739bd17 | |||
| c7310ae830 | |||
| fb5120551c | |||
| 42bc3f624a | |||
| 13abbd03fb | |||
| 6dfebb50a3 |
@@ -2,7 +2,7 @@
|
||||
|
||||
> Jav-鉴黄师 油猴脚本 — 工程化重构版本
|
||||
|
||||
基于 `vite-plugin-monkey` 脚手架,将原 10,000+ 行单体脚本拆分为 49 个可维护的 ES Module 模块,支持本地开发热更新、一键构建。
|
||||
基于 `vite-plugin-monkey` 脚手架,将原 10,000+ 行单体脚本拆分为 56 个可维护的 ES Module 模块,支持本地开发热更新、一键构建。
|
||||
|
||||
---
|
||||
|
||||
@@ -23,20 +23,6 @@
|
||||
|
||||
---
|
||||
|
||||
## 自动部署
|
||||
|
||||
本项目使用 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 弹出安装提示
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 功能概览
|
||||
|
||||
### 列表页增强
|
||||
@@ -53,6 +39,7 @@ git push → GitHub Actions 自动构建 → 发布 GitHub Release
|
||||
- **磁力排序与过滤(JavDB)**:按发布日期/文件大小/文件数量拖拽排序,勾选启用;高清/4K/字幕/无码多条件并集过滤,4K 磁力行高亮显示
|
||||
- 磁力链接高亮匹配已收藏/已下载状态
|
||||
- DMM 多画质预览视频
|
||||
- **多源视频预览**:Javxy 聚合 API 独立按钮(番号/识别码右侧),多源 fallback(JavTrailers / JavDB 等),HLS 支持、播放进度记忆、画质切换
|
||||
- **标题翻译**:Google 翻译,支持开关多次切换不失效
|
||||
- **字幕搜索**:合并迅雷 + SubTitleCat 为单一入口,支持 115 网盘直传、详情页预匹配目录、多目录选择上传、长目录名截断展示、字幕预览绿色字体
|
||||
- **多源预览图**:javfree / projectjav / javstore 三源切换,来源按钮绿色高亮,sessionStorage 缓存,错误提示弹窗内显示
|
||||
@@ -61,11 +48,16 @@ git push → GitHub Actions 自动构建 → 发布 GitHub Release
|
||||
|
||||
### 115 网盘集成
|
||||
|
||||
- **番号智能匹配**:双阶段搜索策略 — 第一阶段精准搜索(完整番号 + `type:4`),未命中时第二阶段模糊兜底(尾号 +
|
||||
正则匹配),支持不规则文件名(如 `MIVR00345-5.mp4`、`ABP001.mp4`)
|
||||
- **不规则文件名自动重命名**
|
||||
:检测到需第二阶段才能匹配的作品时,自动将文件重命名为标准番号格式(如 `mdvr00317-3.mp4` → `MDVR-317-3.mp4`
|
||||
),设置面板可开关 + 选择范围(仅VR / 全部)
|
||||
- 作品目录匹配与详情页预加载
|
||||
- 多目录选择上传字幕
|
||||
- 字幕一键直传至匹配目录
|
||||
- **作品删除增强**:删除整个文件夹(非仅视频),支持标记删除批量操作,删除后字幕目录列表联动;批量删除含风控保护(1~3s
|
||||
随机延迟 + 触发风控自动重试退避)
|
||||
随机延迟 + 触发风控自动重试退避);删除安全检查使用 `codeParse` 正则匹配番号
|
||||
|
||||
### 数据管理
|
||||
|
||||
@@ -108,17 +100,20 @@ JHS-enhance/
|
||||
│ │ ├── storage.js # IndexedDB 存储管理器(localforage 封装,含缓存)
|
||||
│ │ ├── utils.js # 工具函数集(DOM 操作、弹窗确认、快捷键、重试、日期格式化)
|
||||
│ │ └── gmHttp.js # GM_xmlhttpRequest 封装(GET/POST/Form/File、超时重试、分块下载)
|
||||
│ ├── api/ # API 层(2 个模块)
|
||||
│ ├── api/ # API 层(4 个模块)
|
||||
│ │ ├── javdb.js # JavDB API 封装(签名算法、搜索、评论、磁力列表)
|
||||
│ │ └── dmm.js # DMM 预览视频获取(多画质、默认画质选择)
|
||||
│ │ ├── dmm.js # DMM 预览视频获取(多画质、默认画质选择)
|
||||
│ │ ├── dmm-graphql.js # DMM GraphQL API(备选视频源回退)
|
||||
│ │ └── javxy.js # Javxy 聚合视频 API(多源解析、签名生成)
|
||||
│ ├── lib/ # 第三方补丁库
|
||||
│ │ └── gm-xhr-parallel.js # Tampermonkey MV3 并行请求修复(redirect: manual)
|
||||
│ └── plugins/ # 插件层(41 个功能插件)
|
||||
│ │ ├── 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 页:基础/任务/域名/快捷键/屏蔽/缓存/标记删除/打赏作者)
|
||||
│ ├── setting.js # 设置面板(Tab 页:基础/任务/域名/快捷键/屏蔽/缓存/标记删除/打赏作者;含 115 自动重命名开关 + 范围选择)
|
||||
│ ├── translate.js # 标题翻译(Google 翻译 API,原始标题缓存)
|
||||
│ ├── preview-video.js # DMM 预览视频播放器
|
||||
│ ├── highlight-magnet.js # 磁力链接高亮 + 排序 + 过滤(多键排序、并集过滤、4K 高亮)
|
||||
@@ -128,7 +123,7 @@ JHS-enhance/
|
||||
│ ├── new-video.js # 新作品检测与通知
|
||||
│ ├── task.js # 后台任务调度(黑名单/收藏演员/新作品定时执行)
|
||||
│ ├── history.js # 浏览历史表格
|
||||
│ ├── wangpan-115.js # 115 网盘匹配/标签/删除(含风控保护的批量删除)
|
||||
│ ├── wangpan-115.js # 115 网盘匹配/标签/删除(含 codeParse 正则匹配、双阶段搜索、自动重命名)
|
||||
│ ├── wangpan-115-task.js # 115 网盘任务处理(文件搜索 API 封装)
|
||||
│ ├── aliyun-pan.js # 阿里云盘备份/恢复
|
||||
│ ├── fc2.js # FC2 页面处理
|
||||
@@ -152,6 +147,7 @@ JHS-enhance/
|
||||
│ ├── magnet-hub.js # MagnetHub 磁力搜索
|
||||
│ ├── 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 # 想看/已看视频列表
|
||||
@@ -289,7 +285,7 @@ window.show.ok('操作成功'); // Toast 通知
|
||||
|
||||
## 赞助
|
||||
|
||||
如果当前项目有帮到您的话,赞助我喝瓶水 :)
|
||||
如果当前项目有帮到您的话,赞助我喝瓶水/Star项目也是极好的,感谢支持 :)
|
||||
|
||||

|
||||
|
||||
@@ -306,6 +302,22 @@ window.show.ok('操作成功'); // Toast 通知
|
||||
|
||||
## 版本历史
|
||||
|
||||
- **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 + 来源切换弹窗 + 绿色高亮)
|
||||
|
||||
Vendored
+15528
-12208
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,
|
||||
};
|
||||
+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
@@ -54,14 +54,21 @@ class DetailPageButtonPlugin extends BasePlugin {
|
||||
settings = {};
|
||||
}
|
||||
const sortEnabled = settings.sortEnabled || {};
|
||||
const fullSortOrder = (settings.sortOrder && settings.sortOrder.length === 3) ? settings.sortOrder : ["date", "size", "files"];
|
||||
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]);
|
||||
highlightMagnetPlugin.applySortAndFilter({
|
||||
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 {
|
||||
@@ -99,14 +106,23 @@ class DetailPageButtonPlugin extends BasePlugin {
|
||||
} catch (e) {
|
||||
settings = {};
|
||||
}
|
||||
const sortEnabled = settings.sortEnabled || {date: !1, size: !1, files: !1};
|
||||
const sortOrder = (settings.sortOrder && settings.sortOrder.length === 3) ? settings.sortOrder : ["date", "size", "files"];
|
||||
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 = {date: "发布日期", size: "文件大小", files: "文件数量"};
|
||||
const SORT_HINTS = {date: "从新到旧", size: "从大到小", files: "从少到多"};
|
||||
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},
|
||||
|
||||
@@ -7,15 +7,28 @@ class HighlightMagnetPlugin extends BasePlugin {
|
||||
}
|
||||
|
||||
parseDateFromAttr(dateStr) {
|
||||
if (!dateStr || dateStr.length !== 8) {
|
||||
const d = new Date(dateStr);
|
||||
return isNaN(d.getTime()) ? 0 : d.getTime();
|
||||
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 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();
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -42,7 +55,33 @@ class HighlightMagnetPlugin extends BasePlugin {
|
||||
};
|
||||
}
|
||||
|
||||
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;
|
||||
const $container = $("#magnets-content");
|
||||
const $rows = $container.find(".item[data-rank]");
|
||||
@@ -103,6 +142,61 @@ class HighlightMagnetPlugin extends BasePlugin {
|
||||
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 => {
|
||||
@@ -111,7 +205,10 @@ class HighlightMagnetPlugin extends BasePlugin {
|
||||
}));
|
||||
}
|
||||
if (isJavBus) {
|
||||
$("#magnet-table tr").toArray().forEach((el => $(el).show()));
|
||||
$("#magnet-table tr").toArray().forEach((el => {
|
||||
$(el).show();
|
||||
$(el).removeClass('magnet-4k-highlight');
|
||||
}));
|
||||
}
|
||||
}
|
||||
handleBus() {
|
||||
|
||||
@@ -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};
|
||||
+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",
|
||||
|
||||
+17
-5
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);
|
||||
};
|
||||
|
||||
|
||||
+185
-60
@@ -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("");
|
||||
@@ -406,54 +412,39 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
|
||||
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 (let d = 0; d < uniqueDirIds.length; d++) {
|
||||
if (d > 0) {
|
||||
const groupedByDir = {};
|
||||
for (const m of safeMatchList) {
|
||||
if (!groupedByDir[m.dirId]) groupedByDir[m.dirId] = [];
|
||||
groupedByDir[m.dirId].push(m);
|
||||
}
|
||||
const entries = Object.entries(groupedByDir);
|
||||
for (let g = 0; g < entries.length; g++) {
|
||||
if (g > 0) {
|
||||
await randomDelay();
|
||||
}
|
||||
const dirId = uniqueDirIds[d];
|
||||
let retryCount = 0;
|
||||
const maxRetries = 3;
|
||||
while (retryCount <= maxRetries) {
|
||||
try {
|
||||
const result = await gmHttp.postForm("https://webapi.115.com/rb/delete", {
|
||||
"fid[0]": dirId,
|
||||
ignore_warn: "1"
|
||||
});
|
||||
if (result && result.state) {
|
||||
const [dirId, items] = entries[g];
|
||||
if (items.length === 1) {
|
||||
const result = await this._deleteOne(dirId, false, i, selectedItems, item, onProgress);
|
||||
if (result) {
|
||||
itemDeleted++;
|
||||
deletedCount++;
|
||||
}
|
||||
} else {
|
||||
for (let f = 0; f < items.length; f++) {
|
||||
if (f > 0) await randomDelay();
|
||||
const fileDeleted = await this._deleteOne(items[f].folderId, true, i, selectedItems, item, onProgress);
|
||||
if (fileDeleted) {
|
||||
itemDeleted++;
|
||||
deletedCount++;
|
||||
break;
|
||||
} else {
|
||||
const errMsg = (result && result.error) || "未知错误";
|
||||
if (retryCount < maxRetries && this.isRateLimited(result)) {
|
||||
retryCount++;
|
||||
const backoff = (retryCount + 1) * 2000 + Math.random() * 1000;
|
||||
onProgress && onProgress(i + 1, selectedItems.length, item, `风控触发,${backoff / 1000}秒后重试(${retryCount}/${maxRetries})`);
|
||||
await new Promise(resolve => setTimeout(resolve, backoff));
|
||||
continue;
|
||||
}
|
||||
console.error(`删除目录失败: ${dirId}`, errMsg);
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
if (retryCount < maxRetries && this.isRateLimited(e)) {
|
||||
retryCount++;
|
||||
const backoff = (retryCount + 1) * 2000 + Math.random() * 1000;
|
||||
onProgress && onProgress(i + 1, selectedItems.length, item, `风控触发,${backoff / 1000}秒后重试(${retryCount}/${maxRetries})`);
|
||||
await new Promise(resolve => setTimeout(resolve, backoff));
|
||||
continue;
|
||||
}
|
||||
console.error(`删除目录失败: ${dirId}`, e);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -482,9 +473,48 @@ const _WangPan115MatchPlugin = class _WangPan115MatchPlugin extends BasePlugin {
|
||||
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中删除。`);
|
||||
@@ -537,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} 个文件夹`);
|
||||
@@ -573,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>`);
|
||||
@@ -651,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