mirror of
https://github.com/fireinrain/JHS-enhance.git
synced 2026-10-05 09:56:37 +08:00
修复预览视频播放 更新文档
This commit is contained in:
@@ -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,
|
||||
};
|
||||
Reference in New Issue
Block a user