[\s\S]*?<\/div><\/div>/, '');
+ const players = c.parsePlayers(source);
+ assert.deepEqual(plain(players.map(x => [x.lineId, x.line])), [['2','playmate'],['3','luluvdoo'],['4','SW']]);
+ assert.equal(players[0].url, 'https://player2.test/e/film');
+ const groups = players.map(x => c.qualityGroup('https://javgg.net/jav/a/', 'A', x, [{name:'720p',height:720}]));
+ assert.equal(new Set(groups.map(x => x.id)).size, 3);
+});
+
+test('JAVGG discovers packed HLS qualities while hung and empty players fail independently', async () => {
+ const c = load('javgg-mini-library.js', { setTimeout: (fn, ms) => setTimeout(fn, ms / 100) });
+ let active = 0, max = 0, browsers = 0;
+ c.Widget.browser = { async fetch() { browsers++; throw new Error('should not be needed'); } };
+ c.Widget.http.get = async url => {
+ if (url.includes('/jav/')) return {statusCode:200,data:javggDetail()};
+ if (url.includes('player1')) return new Promise(() => {});
+ active++; max = Math.max(active, max); await Promise.resolve(); active--;
+ if (url.includes('player2')) return {statusCode:200,data:'dynamic player'};
+ if (url.includes('player3')) return {statusCode:200,data:packedJavgg('lulu')};
+ if (url.includes('player4')) return {statusCode:200,data:packedJavgg('sw')};
+ return {statusCode:200,data:url.includes('lulu') ? master.split('#EXT-X-STREAM-INF:BANDWIDTH=900000')[0] : master};
+ };
+ const groups = await c.getResourceVersions(JSON.stringify({detailUrl:'https://javgg.net/jav/a/'}));
+ assert.deepEqual(plain(groups.map(x => x.title)), ['luluvdoo 线路','SW 线路']);
+ assert.deepEqual(plain(groups[1].versions.map(x => x.name)), ['1080p','720p']);
+ assert.equal(groups[1].versions.filter(x => x.default).length, 1);
+ assert.equal(browsers, 0); assert.ok(max <= 2);
+ const payload = c.decodePayload(groups[1].versions[1].id);
+ assert.equal(payload.height,720); assert.equal(payload.lineId,'4'); assert.equal(payload.playerUrl,'https://player4.test/e/film');
+ assert.ok(!groups[1].versions[0].id.includes('sign'));
+});
+
+test('JAVGG refreshes signed packed URLs and selects highest / requested / missing quality', async () => {
+ const c = load('javgg-mini-library.js'); let refreshes = 0;
+ c.Widget.http.get = async url => url.includes('player.test') ? {status:200,data:packedJavgg('cdn','master','fresh'+(++refreshes))} : {status:200,data:master};
+ for (const height of [0,720,2160,1080]) {
+ const result = await c.resolvePlayback({versionId:c.encodePayload({kind:'play',detailUrl:'https://javgg.net/jav/a/',playerUrl:'https://player.test/e/a',line:'Server',height})});
+ assert.equal(result.url, 'https://cdn.test/' + (height === 720 ? '720' : '1080') + '.m3u8');
+ }
+ assert.equal(refreshes,4);
+});
+
+test('JAVGG does not execute packed scripts or accept unbounded packing dictionaries', () => {
+ const c = load('javgg-mini-library.js');
+ assert.equal(c.extractPlayableURL(packedJavgg()),'https://cdn.test/master.m3u8?sign=fresh');
+ assert.equal(c.extractPlayableURL(packedJavgg().replace('return p;', 'globalThis.compromised=true;return p;')),'https://cdn.test/master.m3u8?sign=fresh');
+ assert.equal(c.compromised,undefined);
+ assert.equal(c.extractPlayableURL(packedJavgg().replace(',36,10,', ',36,200000,')), '');
+});
+
+test('JAVGG rejects expired 404 manifests, retries fresh discovery, and leaves detail resources empty', async () => {
+ const c = load('javgg-mini-library.js'); let available = false;
+ c.Widget.http.get = async url => url.includes('/jav/') ? {status:200,data:javggDetail(['SW'])} : url.includes('player1') ? {status:200,data:packedJavgg()} : {statusCode:available ? 200 : 404,data:available ? media : 'Not Found'};
+ const ctx={detailUrl:'https://javgg.net/jav/a/'};
+ assert.equal((await c.getDetail(ctx)).resourceGroups.length,0);
+ await assert.rejects(c.getResourceVersions(ctx), /manifest-http-404/);
+ available=true;
+ const groups=await c.getResourceVersions(ctx);assert.equal(groups[0].versions[0].name,'HLS 原始画质');
+});
+
+test('JAVGG captures real browser request headers once, then resolves a lower quality', async () => {
+ const c=load('javgg-mini-library.js'); let browsers=0; const manifestHeaders=[];
+ c.Widget.http.get=async (url, options)=>{
+ if(url.includes('/jav/')) return {status:200,data:javggDetail(['playmate'])};
+ if(url.includes('player1')) return {status:200,data:'dynamic'};
+ manifestHeaders.push(options.headers);return {status:200,data:master};
+ };
+ c.Widget.browser={async fetch(url,options){browsers++;assert.equal(options.visible,false);assert.equal(options.waitForAny,undefined);assert.ok(options.timeout<=10);return {capturedRequests:[{url:'https://cdn.test/master.m3u8?fresh=1',requestHeaders:{referer:'https://actual-player.test/',origin:'https://actual-player.test','user-agent':'Actual Agent',cookie:'fixture-only','Authorization':'must-not-forward'}}]};}};
+ const groups=await c.getResourceVersions({detailUrl:'https://javgg.net/jav/a/'});
+ const result=await c.resolvePlayback(groups[0].versions[1].action);
+ assert.equal(result.url,'https://cdn.test/720.m3u8');assert.equal(browsers,2);
+ assert.deepEqual(plain(result.headers),{'User-Agent':'Actual Agent',Referer:'https://actual-player.test/',Origin:'https://actual-player.test',Cookie:'fixture-only'});
+ assert.ok(manifestHeaders.every(h=>h.Referer==='https://actual-player.test/'));
+});
+
+test('JAVGG bounds a hung native browser and identifies capture failure without exposing tokens', async () => {
+ const c=load('javgg-mini-library.js',{setTimeout:(fn,ms)=>setTimeout(fn,ms/100)});let browsers=0;
+ c.Widget.http.get=async url=>({status:200,data:url.includes('/jav/')?javggDetail(): 'dynamic'});
+ c.Widget.browser={async fetch(){browsers++;return new Promise(()=>{});}};
+ const start=Date.now();await assert.rejects(c.getResourceVersions({detailUrl:'https://javgg.net/jav/a/'}),/browser-media-timeout/);
+ assert.equal(browsers,1);assert.ok(Date.now()-start<500);
+ c.Widget.browser.fetch=async()=>({mediaSources:['blob:https://player.test/id'],secret:'must-not-leak'});
+ await assert.rejects(c.resolvePlayback({playerUrl:'https://player.test/e/a'}),e=>/browser-.*media|browser-blob/.test(e.message)&&!e.message.includes('must-not-leak'));
+});
+
+test('JAVGG quality discovery itself has an overall deadline', async () => {
+ const c=load('javgg-mini-library.js',{setTimeout:fn=>setTimeout(fn,15)});
+ c.discoverResourceVersions=async()=>new Promise(()=>{});
+ await assert.rejects(c.getResourceVersions({}),/画质发现超时.*total-deadline/);
+});
+
+test('JAVGG direct detail Play explores later lines and selects the highest real quality', async () => {
+ const c=load('javgg-mini-library.js');
+ c.Widget.http.get=async url=>{
+ if(url.includes('/jav/')) return {status:200,data:javggDetail(['luluvdoo','SW'])};
+ if(url.includes('player1')) return {status:200,data:packedJavgg('low')};
+ if(url.includes('player2')) return {status:200,data:packedJavgg('high')};
+ return {status:200,data:url.includes('low')?master.replace(/1920x1080/g,'1280x720').replace(/1080.m3u8/g,'720.m3u8'):master};
+ };
+ const result=await c.resolvePlayback({itemId:c.encodePayload({kind:'detail',detailUrl:'https://javgg.net/jav/a/'})});
+ assert.equal(result.url,'https://high.test/1080.m3u8');
+});
+
+test('JAVGG playback fails explicitly when a fresh manifest is permanently unavailable', async () => {
+ const c=load('javgg-mini-library.js');
+ c.Widget.http.get=async url=>url.includes('player.test')?{status:200,data:packedJavgg()}:{statusCode:404,data:'Not Found'};
+ await assert.rejects(c.resolvePlayback({playerUrl:'https://player.test/e/a'}),/manifest-http-404/);
+});
+
+test('JAVGG uses observed luluvdoo headers and does not invent headers for other players', () => {
+ const c=load('javgg-mini-library.js');
+ const known=c.playbackHeaders('https://luluvdoo.com/e/film');
+ assert.equal(known.Referer,'https://luluvdoo.com/');assert.equal(known.Origin,'https://luluvdoo.com');
+ const other=c.playbackHeaders('https://javstreamhq.xyz/e/film');
+ assert.equal(other.Origin,undefined);assert.equal(other.Referer,undefined);
+});
From 583e7670af0770a8baa4b14d5635351b9cf8655b Mon Sep 17 00:00:00 2001
From: hzl <1803573449@qq.com>
Date: Sat, 3 Oct 2026 21:55:14 +0800
Subject: [PATCH 4/6] Bound SexBJCam playback and prefer static player
configuration
---
CHANGELOG.md | 8 +
README.md | 8 +
sexbjcam-mini-library.js | 280 ++++++++++++++++--------------
sexbjcam-network-recovery.test.js | 5 +-
tests/adapters.test.cjs | 104 +++++++++++
5 files changed, 271 insertions(+), 134 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 552a6bf..3448f45 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,11 @@
+# SexBJCam 1.1.9 — 2026-10-03
+
+- 播放器改为 HTTP 静态配置优先,必要时一次浏览器捕获;取消浏览器读取 HLS 正文及三轮画质重试。HTTP/正文和浏览器等待均有脚本侧截止时间,详情、资源发现与播放各限 28 秒。
+- 保留历史条目/画质 ID、媒体库 ID、`baseURL` 和下载路径。每次播放刷新地址,缺失请求画质时选择最高真实画质。
+- 原始 HLS 显示为原始画质,不缓存失败为假默认线路。此前成功的稳定分辨率元数据仍可在临时网络故障时复用;不保存签名 URL 或媒体请求凭证。永久失效、验证页和非 HLS 清单明确失败。
+- 支持 `capturedRequests` 与实际媒体请求头,取消跨来源推测 Origin/Referer 及从任意捕获变体猜造 master 地址。公开 CDN 的最小 User-Agent 流程已通过匿名清单与分片检查。
+- 修正详情标题读取上传者姓名的问题。58 项媒体库回归(新增 12 项 SexBJCam 检查)及网络恢复测试通过;设备浏览器回传和 iPhone 原生起播仍待验收。
+
# 2026-10-03
基于 `main` 的 `8e43017532ec81e3338046ba6e32dff7655bd3dd` 修复审查中 O01–O25;保留所有已发布文件路径。
diff --git a/README.md b/README.md
index 7754c84..bc681d5 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,14 @@ GitHub Actions 会先生成兼容副本并验证;`main` 的检查通过后,
具体行为和设备验证限制见 [CHANGELOG.md](CHANGELOG.md)。
+## SexBJCam 1.1.9 播放修复稿
+
+保留 `sexbjcam-mini-library.js` 原下载路径、`sexbjcam-mini-library` ID、`baseURL` 参数、历史详情 URL 与 `quality:1080/720/480` ID。播放器改为先用 HTTP 读取公开打包配置,必要时只做一次 12 秒浏览器媒体捕获;HLS 正文使用最多 4 秒 HTTP 请求,不再用浏览器导航清单。详情、资源发现和播放各有 28 秒总时限;脚本截止时间不能强制取消宿主原生任务。
+
+画质仅取真实清单或此前成功验证的分辨率元数据,签名地址在播放时刷新。只有原始 HLS 时明确显示“原始画质”,暂时失败不生成假默认线路;404、验证页、非 HLS 和捕获失败显示具体阶段。捕获媒体请求头仅用于本次播放,不写入画质缓存。详情标题优先读取影片标题,避免把嵌套上传者姓名当作标题。
+
+匿名 HTTP 已验证 `recordplay.biz` 和 `playrecord.biz` 的真实 master、1080p/720p/480p 与范围分片;播放器请求也有超时,源站并非始终稳定。匿名主站详情出现过 Cloudflare 403,浏览器会话中可正常读取;不将桌面网页或 HTTP 成功视为 DreamBy/iPhone 原生播放验收。
+
## 本地验收
```bash
diff --git a/sexbjcam-mini-library.js b/sexbjcam-mini-library.js
index 7738b12..8c3fd9c 100644
--- a/sexbjcam-mini-library.js
+++ b/sexbjcam-mini-library.js
@@ -9,7 +9,7 @@ const WidgetMetadata = {
id: 'sexbjcam-mini-library',
name: 'SexBJCam',
title: 'SexBJCam',
- version: '1.1.8',
+ version: '1.1.9',
author: 'Alan huang',
logo: SEXBJCAM_LOGO,
icon: SEXBJCAM_LOGO,
@@ -111,7 +111,7 @@ async function getCategory(rawCtx) {
};
}
-async function getDetail(rawCtx) {
+async function getDetailWithinBudget(rawCtx) {
const ctx = normalizeContext(rawCtx);
const detailURL = detailURLFromContext(ctx);
if (!detailURL) throw new Error('SexBJCam 详情参数无效');
@@ -119,17 +119,14 @@ async function getDetail(rawCtx) {
const detail = parseDetailHtml(ctx, html, detailURL);
const related = parseListHtml(ctx, html).filter(function (item) { return item.id !== detailURL; }).slice(0, 18);
let qualityVersions = [];
- let qualityDiscoveryFailed = false;
if (detail.embedURL) {
try {
qualityVersions = await loadQualityVersions(ctx, detailURL, detail.title, detail.embedURL);
- } catch (_) {
- qualityDiscoveryFailed = true;
- }
+ } catch (_) { /* Dynamic resource loading can retry; do not cache a fake default line. */ }
}
const resourceGroups = qualityVersions.length
? [{ id: 'quality', title: '画质', versions: qualityVersions }]
- : (qualityDiscoveryFailed ? [] : resourceGroupsFor(ctx, detailURL, detail.title, detail.embedURL));
+ : [];
return {
pageType: 'detail', id: detailURL, type: 'movie', title: detail.title,
poster: detail.poster, backdrop: detail.poster, detailImageAspectRatio: '16:9',
@@ -148,7 +145,7 @@ async function getDetail(rawCtx) {
};
}
-async function getResourceVersions(rawCtx) {
+async function getResourceVersionsWithinBudget(rawCtx) {
const ctx = normalizeContext(rawCtx);
const detailURL = detailURLFromContext(ctx);
if (!detailURL) return { itemId: '', groups: [] };
@@ -159,14 +156,11 @@ async function getResourceVersions(rawCtx) {
embedURL = detail.embedURL;
title = detail.title || title;
}
- let versions = [];
- if (embedURL) {
- versions = await loadQualityVersions(ctx, detailURL, title, embedURL);
- }
+ if (!embedURL) throw stageError('detail-player', '没有解析到 SexBJCam 播放器地址');
+ const versions = await loadQualityVersions(ctx, detailURL, title, embedURL);
return {
itemId: detailURL,
- groups: versions.length ? [{ id: 'quality', title: '画质', versions: versions }] :
- resourceGroupsFor(ctx, detailURL, title, embedURL)
+ groups: versions.length ? [{ id: 'quality', title: '画质', versions: versions }] : []
};
}
@@ -182,8 +176,7 @@ async function resolvePlaybackWithinBudget(rawCtx) {
const html = await fetchPlayerText(ctx, embedURL);
const masterURL = extractMediaURL(html, embedURL);
if (!masterURL) throw new Error('没有解析到 SexBJCam HLS 地址');
- const origin = urlOrigin(embedURL);
- const headers = { Referer: embedURL, Origin: origin, 'User-Agent': SEXBJCAM_UA };
+ const headers = ctx.__mediaHeaders || minimalMediaHeaders();
const quality = qualityFromContext(ctx);
let mediaURL = masterURL;
if (/\.m3u8(?:$|[?#])/i.test(masterURL)) {
@@ -192,7 +185,10 @@ async function resolvePlaybackWithinBudget(rawCtx) {
const variants = parseHlsVariants(manifest, masterURL);
const selected = selectVariant(variants, quality);
if (selected && selected.url) mediaURL = selected.url;
- } catch (_) {}
+ } catch (error) {
+ if (/-4\d\d$|verification$|not-hls$|manifest-empty$/.test(error.stage || '')) throw error;
+ // A fresh real URL can still be handed off after a transient quality-probe failure.
+ }
}
return {
url: mediaURL, container: mediaContainer(mediaURL),
@@ -263,8 +259,8 @@ function parseListHtml(ctx, html) {
function parseDetailHtml(ctx, html, detailURL) {
const article = firstMatch(html, /(
]*\bitemprop=["']video["'][^>]*>[\s\S]*?<\/article>)/i)[0] || String(html || '');
const title = cleanText(firstNonEmpty(
- metaItemContent(article, 'name'), metaContent(html, 'property', 'og:title'),
firstMatch(article, /]*>([\s\S]*?)<\/h1>/i)[0],
+ metaContent(html, 'property', 'og:title'), metaItemContent(article, 'name'),
firstMatch(html, /]*>([\s\S]*?)<\/title>/i)[0]
)).replace(/\s*[–|-]\s*sexbjcam\s*$/i, '');
const poster = absoluteURL(ctx, firstNonEmpty(
@@ -292,52 +288,32 @@ function parseDetailHtml(ctx, html, detailURL) {
};
}
-function resourceGroupsFor(ctx, detailURL, title, embedURL) {
- return [{
- id: 'online', title: '在线播放',
- versions: [{
- id: detailURL, name: '默认线路', title: title || '默认线路',
- subtitle: '动态解析 HLS', container: 'm3u8', default: true,
- action: {
- type: 'play', itemId: detailURL, versionId: detailURL,
- embedURL: embedURL || undefined, title: title || '默认线路'
- }
- }]
- }];
-}
-
async function loadQualityVersions(ctx, detailURL, title, embedURL) {
let variants = [];
- let lastError = null;
- let manifestResolved = false;
- let headers = { Referer: embedURL, Origin: urlOrigin(embedURL), 'User-Agent': SEXBJCAM_UA };
- for (let attempt = 0; attempt < 3 && !variants.length; attempt += 1) {
- try {
- const playerHTML = await fetchPlayerText(ctx, embedURL);
- const masterURL = extractMediaURL(playerHTML, embedURL);
- if (!masterURL) throw new Error('播放器没有返回 HLS 地址');
- const manifest = await fetchSignedManifestText(ctx, masterURL, headers);
- if (!/^\s*#EXTM3U/i.test(manifest)) throw new Error('播放器没有返回有效 HLS 清单');
- manifestResolved = true;
- variants = parseHlsVariants(manifest, masterURL);
- } catch (error) {
- lastError = error;
- variants = [];
- }
- }
- if (variants.length) {
- writeQualityMetadata(embedURL, variants.map(function (variant) {
+ let original = false;
+ try {
+ const playerHTML = await fetchPlayerText(ctx, embedURL);
+ const masterURL = extractMediaURL(playerHTML, embedURL);
+ if (!masterURL) throw stageError('player-media', '播放器没有返回 HLS 地址');
+ const headers = ctx.__mediaHeaders || minimalMediaHeaders();
+ const manifest = await fetchSignedManifestText(ctx, masterURL, headers);
+ variants = parseHlsVariants(manifest, masterURL);
+ original = !variants.length && /#EXTINF|#EXT-X-STREAM-INF/i.test(manifest);
+ if (!variants.length && !original) throw stageError('manifest-empty', 'HLS 清单没有媒体条目');
+ if (variants.length) writeQualityMetadata(embedURL, variants.map(function (variant) {
return { height: variant.height, bandwidth: variant.bandwidth || 0 };
}));
- } else {
- variants = readQualityMetadata(embedURL);
- }
- if (!variants.length && !manifestResolved) {
- const reason = lastError && lastError.message ? ':' + lastError.message : '';
- throw new Error('画质加载失败,请检查网络后重新打开资源列表' + reason);
+ } catch (error) {
+ // Previously proven resolution metadata is a fallback, never a cached signed URL.
+ if (!/-4\d\d$|verification$|not-hls$|manifest-empty$/.test(error.stage || '')) variants = readQualityMetadata(embedURL);
+ if (!variants.length) throw stageError(error.stage || 'quality-discovery', '画质加载失败,请检查网络后重新打开资源列表');
}
- if (!variants.length) return [];
- return buildQualityVersions(detailURL, title, embedURL, headers, variants);
+ if (original) return [{
+ id: 'quality:original', name: 'HLS 原始画质', title: 'HLS 原始画质',
+ subtitle: '源站返回原始 HLS · 播放时刷新地址', container: 'm3u8', default: true,
+ action: { type: 'play', itemId: detailURL, versionId: 'quality:original', embedURL: embedURL, title: title }
+ }];
+ return buildQualityVersions(detailURL, title, embedURL, variants);
}
function readQualityMetadata(embedURL) {
@@ -407,7 +383,7 @@ function qualityCacheKey(embedURL) {
return 'sexbjcam:qualities:v1:' + (hash >>> 0).toString(16);
}
-function buildQualityVersions(detailURL, title, embedURL, headers, variants) {
+function buildQualityVersions(detailURL, title, embedURL, variants) {
return variants.map(function (variant, index) {
const qualityId = 'quality:' + variant.height;
return {
@@ -420,7 +396,6 @@ function buildQualityVersions(detailURL, title, embedURL, headers, variants) {
quality: variant.height + 'P',
container: 'm3u8',
default: index === 0,
- headers: headers,
action: {
type: 'play', itemId: detailURL, versionId: qualityId,
quality: variant.height, embedURL: embedURL, title: title || variant.height + 'P'
@@ -460,9 +435,7 @@ function selectVariant(variants, requestedHeight) {
if (!variants || !variants.length) return null;
if (!requestedHeight) return variants[0];
for (let i = 0; i < variants.length; i += 1) if (variants[i].height === requestedHeight) return variants[i];
- return variants.reduce(function (best, item) {
- return Math.abs(item.height - requestedHeight) < Math.abs(best.height - requestedHeight) ? item : best;
- }, variants[0]);
+ return variants[0];
}
function qualityFromContext(ctx) {
@@ -470,7 +443,7 @@ function qualityFromContext(ctx) {
contextValue(ctx, 'quality'), contextValue(ctx, 'height'),
contextValue(ctx, 'versionId'), contextValue(ctx, 'version')
);
- const match = /(?:quality:)?(\d{3,4})p?/i.exec(raw);
+ const match = /^(?:quality:)?(\d{3,4})p?$/i.exec(raw);
return match ? Number(match[1]) : 0;
}
@@ -500,68 +473,96 @@ async function loadSectionItems(ctx, section, page) {
return parseListHtml(ctx, html);
}
-async function fetchText(ctx, url, headers) {
- const response = await httpRequest(url, headers || requestHeaders(ctx, url), remainingPlaybackSeconds(ctx, 6));
- const text = unwrapText(response);
- if (!text) throw new Error('请求失败: ' + url);
- return text;
+async function fetchText(ctx, url, headers, limit, stage) {
+ const seconds = remainingPlaybackSeconds(ctx, limit || 6);
+ return runStage(ctx, stage || 'http', seconds, async function () {
+ let response;
+ try { response = await httpRequest(url, headers || requestHeaders(ctx, url), seconds); }
+ catch (_) { throw stageError(stage || 'http', '来源请求失败'); }
+ const text = await unwrapText(response);
+ const status = Number(response && (response.status || response.statusCode));
+ if (status >= 400) throw stageError((stage || 'http') + '-' + status, '来源请求失败(HTTP ' + status + ')');
+ if (isVerificationPage(text)) throw stageError((stage || 'http') + '-verification', '源站要求浏览器验证');
+ if (!text) throw stageError((stage || 'http') + '-empty', '来源返回空内容');
+ return text;
+ });
}
async function fetchPlayerText(ctx, url) {
const referer = detailURLFromContext(ctx) || baseURL(ctx) + '/';
- const freshHeaders = {
- Referer: referer,
- 'User-Agent': SEXBJCAM_UA,
+ const headers = {
+ Referer: referer, 'User-Agent': SEXBJCAM_UA,
Accept: 'text/html,application/xhtml+xml',
- 'Cache-Control': 'no-cache, no-store, max-age=0',
- Pragma: 'no-cache'
+ 'Cache-Control': 'no-cache, no-store, max-age=0', Pragma: 'no-cache'
};
+ let lastError;
+ try {
+ const text = await fetchText(ctx, cacheBustedURL(url), headers, 6, 'player-http');
+ if (extractMediaURL(text, url)) {
+ ctx.__mediaHeaders = minimalMediaHeaders();
+ return text;
+ }
+ lastError = stageError('player-media', '播放器页面未包含媒体配置');
+ } catch (error) { lastError = error; }
const widget = typeof Widget !== 'undefined' ? Widget : null;
- if (widget && widget.browser && typeof widget.browser.fetch === 'function') {
+ if (!widget || !widget.browser || typeof widget.browser.fetch !== 'function') throw lastError;
+ const seconds = playbackBrowserSeconds(ctx, 12, 1);
+ return runStage(ctx, 'browser-media', seconds, async function () {
+ let result;
try {
- const result = await widget.browser.fetch(cacheBustedURL(url), {
- visible: false,
- timeout: playbackBrowserSeconds(ctx, 12, 2),
- timeoutSeconds: remainingPlaybackSeconds(ctx, 12),
- waitAfterLoad: 3,
- waitForAny: true,
- waitForMediaSource: true,
- headers: freshHeaders
+ result = await widget.browser.fetch(cacheBustedURL(url), {
+ visible: false, timeout: seconds, timeoutSeconds: seconds,
+ waitAfterLoad: 1, waitForMediaSource: true, headers: headers
});
- const browserText = unwrapText(result);
- if (browserText && extractMediaURL(browserText, url)) return browserText;
- const mediaURL = mediaURLFromBrowserResult(result);
- if (mediaURL) return mediaURL;
- } catch (_) {}
- }
- let normalError = null;
- try {
- const text = await fetchText(ctx, cacheBustedURL(url), freshHeaders);
- if (extractMediaURL(text, url)) return text;
- normalError = new Error('播放器页面未包含媒体配置');
- } catch (error) {
- normalError = error;
- }
- throw normalError || new Error('播放器页面请求失败');
+ } catch (_) { throw stageError('browser-media', '浏览器媒体捕获失败'); }
+ const browserText = await unwrapText(result);
+ const capturedURL = mediaURLFromBrowserResult(result);
+ const explicitURL = extractMediaURL(browserText, url);
+ const mediaURL = /\/master\.m3u8(?:$|[?#])/i.test(capturedURL) ? capturedURL : (explicitURL || capturedURL);
+ if (mediaURL) {
+ ctx.__mediaHeaders = capturedMediaHeaders(result, mediaURL) || minimalMediaHeaders();
+ return mediaURL;
+ }
+ const keys = Object.keys(result || {}).slice(0, 12).join(',');
+ const blobOnly = /blob:https?:/.test(JSON.stringify(result && (result.mediaSources || result.capturedRequests || result.mediaRequests) || []));
+ const stage = isVerificationPage(browserText) ? 'browser-verification' : (blobOnly ? 'browser-blob-only' : 'browser-no-media');
+ throw stageError(stage, '浏览器未回传可用媒体;blobOnly=' + blobOnly + ';keys=' + keys);
+ });
}
async function fetchSignedManifestText(ctx, url, headers) {
- const widget = typeof Widget !== 'undefined' ? Widget : null;
- if (widget && widget.browser && typeof widget.browser.fetch === 'function') {
- try {
- const result = await widget.browser.fetch(url, {
- visible: false,
- timeout: playbackBrowserSeconds(ctx, 6, 2),
- timeoutSeconds: remainingPlaybackSeconds(ctx, 6),
- waitAfterLoad: 1,
- waitForAny: true,
- headers: headers
- });
- const browserText = unwrapText(result);
- if (/^\s*#EXTM3U/i.test(browserText)) return browserText;
- } catch (_) {}
+ // Navigating a browser to a signed playlist is not a reliable text fetch.
+ const text = await fetchText(ctx, url, headers, 4, 'manifest-http');
+ if (!/^\s*#EXTM3U/i.test(text)) throw stageError('manifest-not-hls', '来源未返回 HLS 清单');
+ if (!/#EXTINF|#EXT-X-STREAM-INF/i.test(text)) throw stageError('manifest-empty', 'HLS 清单没有媒体条目');
+ return text;
+}
+
+function capturedMediaHeaders(result, url) {
+ const arrays = [result && result.capturedRequests, result && result.mediaRequests, result && result.requests];
+ for (let a = 0; a < arrays.length; a += 1) {
+ if (!Array.isArray(arrays[a])) continue;
+ const request = arrays[a].find(function (item) { return item && item.url === url; });
+ const input = request && (request.requestHeaders || request.headers);
+ if (!input || typeof input !== 'object') continue;
+ const headers = { 'User-Agent': SEXBJCAM_UA };
+ Object.keys(input).forEach(function (key) {
+ const canonical = { 'user-agent': 'User-Agent', referer: 'Referer', origin: 'Origin', cookie: 'Cookie' }[key.toLowerCase()];
+ if (canonical) headers[canonical] = String(input[key]);
+ });
+ return headers;
}
- return fetchText(ctx, url, headers);
+ return null;
+}
+
+function minimalMediaHeaders() {
+ const headers = { 'User-Agent': SEXBJCAM_UA };
+ // The verified static CDN flow works with this User-Agent; captured requests carry their own headers.
+ return headers;
+}
+
+function isVerificationPage(text) {
+ return /Just a moment|Checking (?:your )?browser|cf-browser-verification|cf-chl-/i.test(String(text || '').slice(0, 30000));
}
function mediaURLFromBrowserResult(result) {
@@ -572,7 +573,7 @@ function mediaURLFromBrowserResult(result) {
const direct = stringValue(result[directKeys[i]]);
if (isMediaURL(direct) && !/^blob:/i.test(direct)) candidates.push(direct);
}
- const arrays = [result.mediaSources, result.mediaRequests, result.requests, result.responses, result.urls];
+ const arrays = [result.capturedRequests, result.mediaSources, result.mediaRequests, result.requests, result.responses, result.urls];
for (let a = 0; a < arrays.length; a += 1) {
const values = arrays[a];
if (!Array.isArray(values)) continue;
@@ -586,19 +587,9 @@ function mediaURLFromBrowserResult(result) {
}
}
for (let i = 0; i < candidates.length; i += 1) if (/\/master\.m3u8(?:$|[?#])/i.test(candidates[i])) return candidates[i];
- for (let i = 0; i < candidates.length; i += 1) {
- const masterURL = masterURLFromVariant(candidates[i]);
- if (masterURL) return masterURL;
- }
return candidates[0] || '';
}
-function masterURLFromVariant(url) {
- const value = stringValue(url);
- if (!/\/index-[^/?#]+\.m3u8(?:$|[?#])/i.test(value)) return '';
- return value.replace(/\/index-[^/?#]+\.m3u8(?=[$?#])/i, '/master.m3u8');
-}
-
function cacheBustedURL(url) {
SEXBJCAM_REQUEST_NONCE += 1;
const separator = String(url || '').indexOf('?') >= 0 ? '&' : '?';
@@ -610,8 +601,9 @@ async function httpRequest(url, headers, timeout = 6) {
const dollar = typeof $http !== 'undefined' ? $http : null;
const client = (widget && widget.http) || dollar;
if (!client) throw new Error('当前环境没有可用的 HTTP 客户端');
- if (typeof client.get === 'function') return client.get(url, { headers: headers, timeout, timeoutSeconds: timeout });
- if (typeof client.request === 'function') return client.request({ url: url, method: 'GET', headers: headers, timeout, timeoutSeconds: timeout });
+ const options = { headers: headers, timeout: timeout, timeoutSeconds: timeout, useBrowserFallback: false, browserFallback: false, useBrowserCookie: true, attachBrowserCookie: true };
+ if (typeof client.get === 'function') return client.get(url, options);
+ if (typeof client.request === 'function') return client.request(Object.assign({ url: url, method: 'GET' }, options));
throw new Error('当前 HTTP 客户端不支持 GET');
}
@@ -820,16 +812,32 @@ const SEXBJCAM_API = {
if (typeof globalThis !== 'undefined') Object.keys(SEXBJCAM_API).forEach(function (key) { globalThis[key] = SEXBJCAM_API[key]; });
if (typeof module !== 'undefined' && module.exports) module.exports = SEXBJCAM_API;
-async function withPlaybackBudget(input, operation) {
- let ctx = input;
- if (typeof ctx === 'string') { try { ctx = JSON.parse(ctx); } catch (_) { ctx = {}; } }
- ctx = Object.assign({}, ctx && typeof ctx === 'object' ? ctx : {});
+function stageError(stage, message) {
+ const error = new Error(message + ';stage=' + stage);
+ error.stage = stage;
+ return error;
+}
+
+async function runStage(ctx, stage, seconds, operation) {
+ const budget = ctx && ctx.__playbackBudget;
+ if (budget && budget.expired) throw stageError('total-deadline', '解析超时');
+ let timer;
+ try {
+ const timeout = new Promise(function (_, reject) {
+ timer = setTimeout(function () { reject(stageError(stage + '-timeout', '来源请求超时')); }, seconds * 1000);
+ });
+ return await Promise.race([Promise.resolve().then(operation), timeout]);
+ } finally { clearTimeout(timer); }
+}
+
+async function withPlaybackBudget(input, operation, label) {
+ const ctx = Object.assign({}, normalizeContext(input));
const budget = { endAt: Date.now() + 28000, expired: false, browserCalls: 0 };
ctx.__playbackBudget = budget;
let timer;
try {
const timeout = new Promise(function (_, reject) {
- timer = setTimeout(function () { budget.expired = true; reject(new Error('播放解析超时;stage=total-deadline;请稍后重试')); }, 28000);
+ timer = setTimeout(function () { budget.expired = true; reject(stageError('total-deadline', (label || '播放解析') + '超时;请稍后重试')); }, 28000);
});
return await Promise.race([Promise.resolve().then(function () { return operation(ctx); }), timeout]);
} finally { clearTimeout(timer); budget.expired = true; }
@@ -854,3 +862,11 @@ function playbackBrowserSeconds(ctx, limit, maxCalls) {
async function resolvePlayback(input) {
return withPlaybackBudget(input, resolvePlaybackWithinBudget);
}
+
+async function getDetail(input) {
+ return withPlaybackBudget(input, getDetailWithinBudget, '详情加载');
+}
+
+async function getResourceVersions(input) {
+ return withPlaybackBudget(input, getResourceVersionsWithinBudget, '画质发现');
+}
diff --git a/sexbjcam-network-recovery.test.js b/sexbjcam-network-recovery.test.js
index 49a9e58..79f30c7 100644
--- a/sexbjcam-network-recovery.test.js
+++ b/sexbjcam-network-recovery.test.js
@@ -27,14 +27,15 @@ global.Widget = {
http: {
get(url) {
if (String(url).indexOf(detailURL) === 0) return detailHTML;
+ if (networkGood && String(url).indexOf(masterURL) === 0) return masterManifest;
throw new Error('network unavailable');
}
},
browser: {
fetch(url) {
if (!networkGood) throw new Error('network unavailable');
- if (String(url).indexOf(embedURL) === 0) return '';
- if (String(url).indexOf(masterURL) === 0) return masterManifest;
+ if (String(url).indexOf(embedURL) === 0) return { html: '', capturedRequests: [{ url: masterURL, requestHeaders: { Referer: embedURL, Origin: 'https://player.example', 'User-Agent': 'Fixture browser' } }] };
+ if (String(url).indexOf(masterURL) === 0) throw new Error('不得用浏览器导航读取 HLS 正文');
throw new Error('unexpected browser URL: ' + url);
}
},
diff --git a/tests/adapters.test.cjs b/tests/adapters.test.cjs
index eba6a24..ed79d68 100644
--- a/tests/adapters.test.cjs
+++ b/tests/adapters.test.cjs
@@ -257,3 +257,107 @@ test('XVideos attaches the configured session token without printing it', async
const options=await vm.runInContext('widgetAPI.getDefaultOptions()',c);
assert.equal(options.headers.Cookie,'session_token='+token); assert.ok(logs.every(line=>!line.includes(token)));
});
+
+const sexDetailURL='https://sexbjcam.com/2026/09/30/sample/';
+const sexEmbedURL='https://player.test/embed/sample';
+const sexDetailHTML='Film title
';
+function packedSexPlayer(token='fresh') {
+ return `eval(function(p,a,c,k,e,d){return p;}('0="1://2.3/4.5?6=7";',36,8,'source|https|cdn|test|master|m3u8|token|${token}'.split('|'),0,{}))`;
+}
+
+test('SexBJCam reads the film title rather than nested uploader metadata', () => {
+ const c=load('sexbjcam-mini-library.js');
+ const detail=c.parseDetailHtml({},sexDetailHTML,sexDetailURL);
+ assert.equal(detail.title,'Film title');assert.equal(detail.embedURL,sexEmbedURL);
+});
+
+test('SexBJCam static packed playback avoids a hung browser and refreshes every selection', async () => {
+ const c=load('sexbjcam-mini-library.js');let browsers=0,players=0;const urls=[];
+ c.Widget.browser={async fetch(){browsers++;return new Promise(()=>{});}};
+ c.Widget.http.get=async (url,options)=>{urls.push(url);assert.equal(options.browserFallback,false);return {statusCode:200,data:url.includes('player.test')?packedSexPlayer('fresh'+(++players)):master};};
+ for(const [id,expected] of [[sexDetailURL,1080],['quality:720',720],['quality:1080',1080],['quality:540',1080]]) {
+ const result=await c.resolvePlayback(JSON.stringify({itemId:sexDetailURL,embedURL:sexEmbedURL,versionId:id}));
+ assert.equal(result.url,'https://cdn.test/'+expected+'.m3u8');
+ }
+ assert.equal(browsers,0);assert.equal(players,4);
+ assert.ok(urls.filter(x=>x.includes('cdn.test')).every(x=>!x.includes('_dreamby_refresh')));
+});
+
+test('SexBJCam hung native HTTP falls back once and preserves captured media headers', async () => {
+ const c=load('sexbjcam-mini-library.js',{setTimeout:(fn,ms)=>setTimeout(fn,ms/100)});let browsers=0;
+ c.Widget.http.get=async (url,options)=>{
+ if(url.includes('player.test'))return new Promise(()=>{});
+ assert.equal(options.headers.Referer,'https://actual-player.test/');return {status:200,data:master};
+ };
+ c.Widget.browser={async fetch(url,options){browsers++;assert.ok(options.timeout<=12);assert.equal(options.waitForAny,undefined);return {capturedRequests:[{url:'https://cdn.test/master.m3u8?token=device',requestHeaders:{referer:'https://actual-player.test/',origin:'https://actual-player.test','user-agent':'Device Agent',cookie:'fixture-cookie',Authorization:'must-not-copy'}}]};}};
+ const result=await c.resolvePlayback({itemId:sexDetailURL,embedURL:sexEmbedURL,versionId:'quality:720'});
+ assert.equal(result.url,'https://cdn.test/720.m3u8');assert.equal(browsers,1);
+ assert.deepEqual(plain(result.headers),{'User-Agent':'Device Agent',Referer:'https://actual-player.test/',Origin:'https://actual-player.test',Cookie:'fixture-cookie'});
+});
+
+test('SexBJCam hung native browser reports its own stage instead of the overall deadline', async () => {
+ const c=load('sexbjcam-mini-library.js',{setTimeout:(fn,ms)=>setTimeout(fn,ms/100)});let browsers=0;
+ c.Widget.http.get=async()=>({statusCode:503,data:'unavailable'});
+ c.Widget.browser={async fetch(){browsers++;return new Promise(()=>{});}};
+ const start=Date.now();await assert.rejects(c.resolvePlayback({embedURL:sexEmbedURL}),/browser-media-timeout/);
+ assert.equal(browsers,1);assert.ok(Date.now()-start<500);
+});
+
+test('SexBJCam stalled quality probe preserves a fresh media URL but creates no fake resource', async () => {
+ const c=load('sexbjcam-mini-library.js',{setTimeout:(fn,ms)=>setTimeout(fn,ms/100)});
+ c.Widget.http.get=async url=>url.includes('/2026/')?{status:200,data:sexDetailHTML}:url.includes('player.test')?{status:200,data:packedSexPlayer()}:new Promise(()=>{});
+ const result=await c.resolvePlayback({itemId:sexDetailURL,embedURL:sexEmbedURL});
+ assert.equal(result.url,'https://cdn.test/master.m3u8?token=fresh');
+ const detail=await c.getDetail({itemId:sexDetailURL});assert.equal(detail.resourceGroups.length,0);
+ await assert.rejects(c.getResourceVersions({itemId:sexDetailURL,embedURL:sexEmbedURL}),/manifest-http-timeout/);
+});
+
+test('SexBJCam variant-only capture keeps its URL and exposes honest original HLS quality', async () => {
+ const c=load('sexbjcam-mini-library.js');const variant='https://cdn.test/index-f3-v1-a1.m3u8?token=exact';
+ c.Widget.http.get=async url=>url.includes('player.test')?{status:200,data:'dynamic player'}:{status:200,data:media};
+ c.Widget.browser={async fetch(){return {capturedRequests:[{url:variant,requestHeaders:{Referer:'https://actual-player.test/'}}]};}};
+ assert.equal(c.mediaURLFromBrowserResult({capturedRequests:[{url:variant}]}),variant);
+ const groups=await c.getResourceVersions({itemId:sexDetailURL,embedURL:sexEmbedURL});
+ assert.equal(groups.groups[0].versions[0].name,'HLS 原始画质');assert.equal(groups.groups[0].versions[0].id,'quality:original');
+ const result=await c.resolvePlayback(groups.groups[0].versions[0].action);assert.equal(result.url,variant);
+});
+
+test('SexBJCam metadata cache never stores signed URLs or media request credentials', async () => {
+ const c=load('sexbjcam-mini-library.js');const stored=[];
+ c.Widget.storage={get(){return null},set(key,value){stored.push(value)}};
+ c.Widget.http.get=async url=>url.includes('player.test')?{status:200,data:'dynamic player'}:{status:200,data:master};
+ c.Widget.browser={async fetch(){return {capturedRequests:[{url:'https://cdn.test/master.m3u8?token=ephemeral',requestHeaders:{Cookie:'private-fixture',Origin:'https://actual-player.test'}}]};}};
+ const groups=await c.getResourceVersions({itemId:sexDetailURL,embedURL:sexEmbedURL});
+ assert.deepEqual(plain(groups.groups[0].versions.map(x=>x.name)),['1080P','720P']);
+ const serialized=JSON.stringify(stored)+JSON.stringify(groups);
+ assert.ok(!serialized.includes('ephemeral'));assert.ok(!serialized.includes('private-fixture'));
+});
+
+test('SexBJCam rejects permanent manifest errors and can retry the same item after recovery', async () => {
+ const c=load('sexbjcam-mini-library.js');let status=404;
+ c.Widget.http.get=async url=>url.includes('player.test')?{status:200,data:packedSexPlayer()}:{statusCode:status,data:status===200?master:'Not Found'};
+ const ctx={itemId:sexDetailURL,embedURL:sexEmbedURL};
+ await assert.rejects(c.resolvePlayback(ctx),/manifest-http-404/);
+ await assert.rejects(c.getResourceVersions(ctx),/manifest-http-404/);
+ status=200;assert.equal((await c.getResourceVersions(ctx)).groups[0].versions[0].name,'1080P');
+});
+
+test('SexBJCam browser failures distinguish blob-only results without leaking response values', async () => {
+ const c=load('sexbjcam-mini-library.js');c.Widget.http.get=async()=>({status:200,data:'dynamic'});
+ c.Widget.browser={async fetch(){return {mediaSources:['blob:https://player.test/id'],secret:'must-not-leak'};}};
+ await assert.rejects(c.resolvePlayback({embedURL:sexEmbedURL}),error=>/browser-blob-only/.test(error.message)&&!error.message.includes('must-not-leak'));
+});
+
+for(const [entry,operation,label] of [['getDetail','getDetailWithinBudget','详情加载'],['getResourceVersions','getResourceVersionsWithinBudget','画质发现']]) {
+ test(`SexBJCam ${entry} has an overall deadline`,async()=>{
+ const c=load('sexbjcam-mini-library.js',{setTimeout:fn=>setTimeout(fn,15)});c[operation]=async()=>new Promise(()=>{});
+ await assert.rejects(c[entry]({}),new RegExp(label+'超时.*total-deadline'));
+ });
+}
+
+test('SexBJCam rejects an empty HLS response rather than returning a fake playable URL', async () => {
+ const c=load('sexbjcam-mini-library.js');
+ c.Widget.http.get=async url=>({status:200,data:url.includes('player.test')?packedSexPlayer():'#EXTM3U\n'});
+ await assert.rejects(c.resolvePlayback({embedURL:sexEmbedURL}),/manifest-empty/);
+ await assert.rejects(c.getResourceVersions({itemId:sexDetailURL,embedURL:sexEmbedURL}),/manifest-empty/);
+});
From 60edba51ba081862a4ab5d395d7f4e0c29e46ec4 Mon Sep 17 00:00:00 2001
From: hzl <1803573449@qq.com>
Date: Sat, 3 Oct 2026 22:32:39 +0800
Subject: [PATCH 5/6] Recover Madou streams and report source access limits
---
CHANGELOG.md | 9 ++
README.md | 10 ++
kbjfan-mini-library.js | 60 +++++++---
madou8-mini-library 5.js | 229 ++++++++++++++++++++++++++-------------
taolusm-mini-library.js | 113 ++++++++++++-------
tests/adapters.test.cjs | 134 +++++++++++++++++++++++
6 files changed, 423 insertions(+), 132 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 552a6bf..2d2f8ee 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,12 @@
+# 麻豆 / 套路 SM / KBJ fan — 2026-10-03
+
+- 麻豆亚洲版 1.1.2 首屏减少为一次最近更新请求,分类预览限两个并发并随后加载。兼容 JSON 上下文/参数、嵌套 HTML 与异步响应正文;错误不再吞成成功空首页。缺少 `URL` 内置对象时仍能保留详情动作与相对 HLS 路径。
+- 普通 HLS 候选由前四条扩大到最多 12 条,限三个逻辑并发和 28 秒总预算;原生调用超时后无法由 JS 强制取消。读取并正常转交 API/公开播放器使用的 `Accept` 凭据,播放时刷新,资源版本不持有媒体凭据或签名 URL。网页专用线路不冒充原生 HLS;未知分辨率显示原始画质,全部失败时不返回假默认线路。
+- 桌面已验证五个分类的两页、搜索两页、三部详情/清单/分片。麻豆 IPX-559 返回有效原始 HLS,但该线路没有分辨率证据,1080P 仍未确认;其他 CDN 仍有 403。另一部影片完成 1080P → 720P → 1080P 的刷新验证。
+- 套路 SM 1.0.2 读取对应影片主播放器,不把下载/登录 HTML 当 MP4,不误取推荐区媒体。真实匿名样本均只提供预览,资源与说明明确标注;旧下载版本仍能识别,权限不足时报明原因。完整影片需要站点正常登录及权限,未宣称修复该限制。
+- KBJ fan 1.0.2 对连接、HTTP 状态、异常跳转、空响应及没有原站影片作明确诊断,HTTP 请求及正文限制为 8 秒。原域名当前连接异常/跳转其他站点,没有确认到替代站点,因此此项为诊断改进,内容未恢复。
+- 保留三个脚本原下载路径、媒体库 ID、原参数名及历史稳定标识。64 项独立媒体库回归(新增 18 项)与仓库 CI 对应检查通过;iPhone 实际显示和原生播放仍待验收。
+
# 2026-10-03
基于 `main` 的 `8e43017532ec81e3338046ba6e32dff7655bd3dd` 修复审查中 O01–O25;保留所有已发布文件路径。
diff --git a/README.md b/README.md
index 7754c84..d6429b8 100644
--- a/README.md
+++ b/README.md
@@ -59,6 +59,16 @@ GitHub Actions 会先生成兼容副本并验证;`main` 的检查通过后,
具体行为和设备验证限制见 [CHANGELOG.md](CHANGELOG.md)。
+## 剩余故障修复稿 — 2026-10-03
+
+- 麻豆亚洲版 `madou8-mini-library 5.js` 为 1.1.2:首屏只取最近更新,分类预览随后加载;HTTP 错误及验证失败明确报告。五个分类前两页各返回 12 条且没有跨页重复;桌面首屏 1.543 秒。原文件名、ID、`baseUrl` 和 `madou8://` 版本载荷保持兼容。
+- 播放检查最多 12 条实际普通 HLS 线路,使用三个逻辑并发任务和 28 秒预算,避免接口顺序变化时漏掉第五条以后的有效源。沿用网页 `xhrSetup` 的 `Accept` 凭据,每次从 API 刷新,不写入版本/缓存。专用 StreamPipe/service-worker 线路及明确 `native:false` 的线路不会伪装成原生地址。
+- 麻豆 IPX-559 已验证原始 HLS 及 MPEG-TS 分片;来源未标明该线路分辨率,没有宣称 1080P 恢复。STCV-497 返回 720P;另一部普通影片返回 1080P/720P/480P/360P,并通过 1080P → 720P → 1080P 的桌面请求。仍有 CDN 返回 403;未通过伪造权限或签名规避。
+- 套路 SM `taolusm-mini-library.js` 为 1.0.2:旧下载入口会跳到登录页,现从当前影片主播放器配置读取实际源。三部匿名 HTTP 样本仅提供公开预览,资源明确标注“公开预览(非完整影片)”;旧下载选择报告登录/观看权限,完整影片未宣称恢复。
+- KBJ fan `kbjfan-mini-library.js` 为 1.0.2:补充 HTTP 状态、响应正文、8 秒截止时间和域名跳转诊断,加载失败不再返回成功的空首页或假播放线路。当前原域名连接中断,另一次网页读取跳到其他站点;没有确认到官方替代地址,内容恢复仍待可用站点。
+
+64 项媒体库回归(新增 18 项)及仓库检查通过。以上真实媒体结果来自桌面 HTTP;DreamBy/iPhone 的显示、原生起播及验证状态衔接仍需设备确认。用户报告的 IPX-559 所属媒体库尚待确认,本轮按麻豆亚洲版检查该影片。原下载路径全部保留;本分支为等待统一确认的修复稿。
+
## 本地验收
```bash
diff --git a/kbjfan-mini-library.js b/kbjfan-mini-library.js
index 3d88485..84f93e0 100644
--- a/kbjfan-mini-library.js
+++ b/kbjfan-mini-library.js
@@ -9,7 +9,7 @@ const WidgetMetadata = {
id: 'kbjfan-mini-library',
name: 'KBJFan',
title: 'KBJFan',
- version: '1.0.1',
+ version: '1.0.2',
author: 'Alan huang',
logo: KBJFAN_LOGO,
icon: KBJFAN_LOGO,
@@ -61,12 +61,7 @@ function getManifest() {
async function getHome(ctx) {
ctx = normalizeRuntimeContext(ctx);
- let hero = [];
- try {
- hero = (await loadSectionItems(ctx, KBJFAN_SECTIONS[0], 1)).slice(0, 8);
- } catch (_) {
- hero = [];
- }
+ const hero = (await loadSectionItems(ctx, KBJFAN_SECTIONS[0], 1)).slice(0, 8);
return {
pageType: 'home',
id: 'kbjfan-home',
@@ -116,6 +111,8 @@ async function getCategory(ctx) {
const section = findSection(ctx && (ctx.pageId || ctx.id || ctx.category || ctx.genreId)) || KBJFAN_SECTIONS[0];
const page = positiveInt(contextValue(ctx, 'page'), 1);
const html = await fetchText(ctx, categoryURL(ctx, section, page));
+ const items = parseListHtml(ctx, html);
+ if (!items.length && page === 1) throw new Error('KBJFan 当前页面没有原站影片,请确认可用站点地址');
return {
pageType: 'category',
id: section.id,
@@ -124,7 +121,7 @@ async function getCategory(ctx) {
itemAspectRatio: '16:9',
page: page,
hasMore: hasNextPage(html, page),
- items: parseListHtml(ctx, html)
+ items: items
};
}
@@ -164,6 +161,7 @@ async function getResourceVersions(ctx) {
videoURL = detail.videoURL;
title = detail.title || title;
}
+ if (!videoURL) throw new Error('KBJFan 当前详情没有可播放媒体,可能是站点已迁移或内容不可用');
return resourceGroupsFor(ctx, detailURL, title, videoURL);
}
@@ -269,6 +267,7 @@ function parseDetailHtml(ctx, html, detailURL) {
}
function resourceGroupsFor(ctx, detailURL, title, videoURL) {
+ if (!videoURL) return [];
return [{
id: 'online',
title: '在线播放',
@@ -287,14 +286,49 @@ function resourceGroupsFor(ctx, detailURL, title, videoURL) {
}
async function loadSectionItems(ctx, section, page) {
- return parseListHtml(ctx, await fetchText(ctx, categoryURL(ctx, section, page)));
+ const items = parseListHtml(ctx, await fetchText(ctx, categoryURL(ctx, section, page)));
+ if (!items.length && page === 1) throw new Error('KBJFan 当前页面没有原站影片;请确认站点地址是否仍提供 KBJFan 内容。');
+ return items;
}
async function fetchText(ctx, url) {
- const response = await Widget.http.get(url, { headers: requestHeaders(ctx, url) });
- const data = response && (response.data || response.body || response.text);
- if (!data) throw new Error('请求失败: ' + url);
- return String(data);
+ try {
+ const response = await kbjDeadline(async function () {
+ const options = { headers: requestHeaders(ctx, url), timeout: 8, timeoutSeconds: 8, browserFallback: false };
+ let response;
+ if (typeof Widget !== 'undefined' && Widget.http && typeof Widget.http.get === 'function') response = await Widget.http.get(url, options);
+ else if (typeof Widget !== 'undefined' && Widget.http && typeof Widget.http.request === 'function') response = await Widget.http.request(Object.assign({ url: url, method: 'GET' }, options));
+ else if (typeof $http !== 'undefined' && typeof $http.get === 'function') response = await $http.get(url, options);
+ else throw new Error('当前环境没有 HTTP 客户端');
+ const final = response && (response.finalURL || response.urlEffective || response.responseURL || response.url);
+ if (final && kbjHost(final) && kbjHost(final) !== kbjHost(baseURL(ctx))) throw new Error('原域名已跳转到其他站点,请更新为已确认的 KBJFan 地址');
+ const status = Number(response && (response.statusCode || response.status) || 200);
+ if (status >= 400) throw new Error('站点返回 HTTP ' + status);
+ const text = await kbjResponseText(response);
+ if (!text) throw new Error('站点返回空内容');
+ if (/Just a moment|Checking (?:your )?browser|cf-chl-|cf-mitigated|Cloudflare Ray ID/i.test(text)) throw new Error('站点返回验证页,未收到影片内容');
+ return text;
+ }, 8000);
+ return response;
+ } catch (error) {
+ throw new Error('KBJFan 加载失败:' + stringValue(error && error.message || error) + '。原下载路径仍保留;无法连接或站点已迁移时需确认可用站点地址。');
+ }
+}
+function kbjHost(url) { const match = String(url || '').match(/^https?:\/\/([^/?#]+)/i); return match ? match[1].toLowerCase().replace(/^www\./, '') : ''; }
+async function kbjResponseText(response) {
+ if (typeof response === 'string') return response;
+ if (!response) return '';
+ if (typeof response.text === 'function') return String(await response.text());
+ for (const value of [response.data, response.body, response.html, response.text]) {
+ if (typeof value === 'string') return value;
+ if (value && typeof value.html === 'string') return value.html;
+ }
+ return '';
+}
+async function kbjDeadline(work, milliseconds) {
+ let timer;
+ try { return await Promise.race([Promise.resolve().then(work), new Promise(function (_, reject) { timer = setTimeout(function () { reject(new Error('HTTP 等待超时(8 秒)')); }, milliseconds); })]); }
+ finally { clearTimeout(timer); }
}
function requestHeaders(ctx, referer) {
diff --git a/madou8-mini-library 5.js b/madou8-mini-library 5.js
index b108309..0a2cf5c 100644
--- a/madou8-mini-library 5.js
+++ b/madou8-mini-library 5.js
@@ -8,7 +8,7 @@ const WidgetMetadata = {
id: 'madou8-mini-library',
name: '麻豆视频',
title: '麻豆视频',
- version: '1.1.1',
+ version: '1.1.2',
requiredVersion: '0.0.1',
author: 'Alan huang',
site: MADOU8_DEFAULT_BASE,
@@ -38,45 +38,38 @@ function getManifest() {
}
async function getHome(ctx) {
- let homeHtml = '';
- let recent = [];
- try {
- homeHtml = await fetchText(ctx, entryURL(ctx));
- recent = parseHomeSectionCards(homeHtml, SECTIONS[0].title, ctx).slice(0, 18);
- } catch (_) {}
- if (!recent.length) {
- try { recent = parseCards(await fetchText(ctx, listURL(ctx, SECTIONS[0], 1)), ctx).slice(0, 18); } catch (_) {}
- }
- const previewPairs = await Promise.all(SECTIONS.map(async function (section) {
- if (section.id === 'recent' && recent.length) return { section: section, items: recent };
- const embedded = parseHomeSectionCards(homeHtml, section.sourceTitle || section.title, ctx).slice(0, 3);
- if (embedded.length) return { section: section, items: embedded };
- try {
- return { section: section, items: parseCards(await fetchText(ctx, listURL(ctx, section, 1)), ctx).slice(0, 3) };
- } catch (_) { return { section: section, items: [] }; }
- }));
- const categoryItems = previewPairs.map(function (pair) {
- return categoryPreviewCard(pair.section, pair.items);
- }).filter(Boolean);
+ ctx = madouContext(ctx, 12000);
+ const recent = parseCards(await fetchText(ctx, listURL(ctx, SECTIONS[0], 1)), ctx).slice(0, 18);
+ if (!recent.length) throw new Error('麻豆首页未收到影片列表;请检查站点地址或网络验证状态');
return {
pageType: 'home', id: 'madou8-home', title: WidgetMetadata.title, heroAspectRatio: '16:9',
hero: recent.slice(0, 6).map(wideItem),
- sections: [{ id: 'madou8-categories', title: '分类浏览', style: 'discover.annualPosterStack', lazy: false, items: categoryItems },
+ sections: [{ id: 'madou8-categories', title: '分类浏览', style: 'discover.annualPosterStack', lazy: true,
+ loadAction: { type: 'custom', id: 'madou8-categories', sectionId: 'madou8-categories' }, items: [] },
{ id: 'recent', title: '最近更新', style: 'discover.ranked', lazy: false, moreAction: categoryAction(SECTIONS[0]), items: ranked(recent) }
].concat(SECTIONS.slice(1).map(sectionShell))
};
}
async function getHomeSection(ctx) {
+ ctx = madouContext(ctx, 20000);
+ if ((ctx.sectionId || ctx.id) === 'madou8-categories') {
+ const pairs = await madouMap(SECTIONS, 2, async function (section) {
+ try { return categoryPreviewCard(section, parseCards(await fetchText(ctx, listURL(ctx, section, 1)), ctx)); } catch (_) { return null; }
+ });
+ const items = pairs.filter(Boolean);
+ return { id: 'madou8-categories', title: '分类浏览', style: 'discover.annualPosterStack', lazy: false, items: items, error: items.length ? undefined : '分类预览加载失败,请稍后重试' };
+ }
const section = findSection(ctx && (ctx.sectionId || ctx.id || ctx.pageId)) || SECTIONS[0];
try {
const items = parseCards(await fetchText(ctx, listURL(ctx, section, 1)), ctx).slice(0, 18);
+ if (!items.length) throw new Error('当前分类未返回影片条目');
return { id: section.id, title: section.title, style: section.style, lazy: false, moreAction: categoryAction(section), items: section.style === 'discover.ranked' ? ranked(items) : items };
} catch (error) { return emptySection(section, error); }
}
async function getCategory(ctx) {
- ctx = normalizeContext(ctx);
+ ctx = madouContext(ctx);
const page = positiveInt(contextValue(ctx, 'page'), 1);
const section = findSection(ctx.pageId || ctx.id) || { id: String(ctx.pageId || ctx.id || 'recent'), title: ctx.title || '麻豆视频', path: ctx.path || 'videos/recent' };
try {
@@ -88,7 +81,7 @@ async function getCategory(ctx) {
}
async function search(ctx) {
- ctx = normalizeContext(ctx);
+ ctx = madouContext(ctx);
const query = String(ctx.query || ctx.keyword || ctx.text || '').trim();
const page = positiveInt(contextValue(ctx, 'page'), 1);
if (!query) return { pageType: 'search', title: '搜索', query: '', page: page, hasMore: false, items: [] };
@@ -98,7 +91,7 @@ async function search(ctx) {
}
async function getDetail(ctx) {
- ctx = normalizeContext(ctx);
+ ctx = madouContext(ctx);
const url = detailURL(ctx);
if (!url) throw new Error('详情参数无效');
const html = await fetchText(ctx, url);
@@ -122,7 +115,7 @@ async function getDetail(ctx) {
}
async function getResourceVersions(ctx) {
- ctx = normalizeContext(ctx);
+ ctx = madouContext(ctx);
const url = detailURL(ctx);
if (!url) return { groups: [] };
const html = await fetchText(ctx, url);
@@ -131,7 +124,7 @@ async function getResourceVersions(ctx) {
}
async function resolvePlayback(ctx) {
- ctx = normalizeContext(ctx);
+ ctx = madouContext(ctx);
const url = detailURL(ctx);
if (!url) throw new Error('播放参数无效:缺少详情地址或视频 ID');
const decoded = decodeVersionId(ctx.versionId);
@@ -145,16 +138,17 @@ async function resolvePlayback(ctx) {
const requestedHeight = positiveInt(ctx.qualityId || decodeVersionId(ctx.versionId).height, 0);
const chosen = await chooseStream(ctx, info.playlist || [], requestedHeight, url);
if (!chosen || !chosen.url) throw new Error('播放解析失败:流接口未返回可播放的 M3U8');
- return { url: chosen.url, container: 'm3u8', headers: playbackHeaders(ctx, url), startPositionSeconds: 0, isLive: false, streamKind: 'vod' };
+ return { url: chosen.url, container: 'm3u8', headers: chosen.headers || playbackHeaders(ctx, url), startPositionSeconds: 0, isLive: false, streamKind: 'hls' };
}
async function buildPlaybackGroups(ctx, detailUrl, uid, title) {
const info = await fetchStreamInfo(ctx, uid, detailUrl);
const variants = await discoverVariants(ctx, info.playlist || [], detailUrl);
- const qualities = variants.length ? variants : [{ height: 0, label: '自动' }];
+ if (!variants.length) throw new Error('没有可验证的 HLS 线路;专用网页播放器线路不能作为原生播放地址');
+ const qualities = variants;
return [{ id: 'hls', title: '在线线路', versions: qualities.map(function (q, index) {
const id = encodeVersionId({ url: detailUrl, uid: uid, height: q.height || 0 });
- return { id: id, name: q.height ? q.height + 'P' : q.label, subtitle: q.height ? '切换时请稍候' : '自动画质', container: 'm3u8', default: index === 0, headers: playbackHeaders(ctx, detailUrl), action: { type: 'play', itemId: videoSlug(detailUrl), detailUrl: detailUrl, videoUid: uid, qualityId: q.height || 0, versionId: id, title: title } };
+ return { id: id, name: q.height ? q.height + 'P' : '原始画质', subtitle: q.height ? '切换时请稍候' : '源清单未标明分辨率', container: 'm3u8', default: index === 0, action: { type: 'play', itemId: videoSlug(detailUrl), detailUrl: detailUrl, videoUid: uid, qualityId: q.height || 0, versionId: id, title: title } };
}) }];
}
@@ -166,64 +160,72 @@ async function fetchStreamInfo(ctx, uid, detailUrl) {
return data;
}
+function nativePlaylist(playlist) {
+ return (playlist || []).filter(function (item) {
+ return item && /^https?:\/\//i.test(String(item.url || '')) && item.native !== false &&
+ (!item.playMode || item.playMode === 'fetchm3u8') && !/\.enc(?:[/:]|$)/i.test(item.url);
+ }).slice(0, 12);
+}
+function streamHeaders(ctx, detailUrl, item) {
+ const headers = playbackHeaders(ctx, detailUrl);
+ // The public player's xhrSetup uses this exact Accept value for signed lines.
+ if (item && typeof item.urlSign === 'string' && item.urlSign) headers.Accept = '*/*;sign=' + encodeURIComponent(item.urlSign);
+ return headers;
+}
async function discoverVariants(ctx, playlist, detailUrl) {
- const discovered = [];
- for (let i = 0; i < Math.min(playlist.length, 4); i++) {
- const url = playlist[i] && playlist[i].url;
- if (!/^https?:\/\//i.test(String(url || ''))) continue;
+ const results = await madouMap(nativePlaylist(playlist), 3, async function (item) {
try {
- const body = await fetchHlsText(ctx, url, detailUrl);
- const variants = parseMaster(body, url);
- if (variants.length) {
- discovered.push.apply(discovered, variants.slice(0, 6));
- } else if (/#EXTINF/i.test(body)) {
- const inferred = inferQuality(url);
- if (inferred) discovered.push(inferred);
- }
+ const headers = streamHeaders(ctx, detailUrl, item);
+ const body = await fetchHlsText(ctx, item.url, detailUrl, headers);
+ const variants = parseMaster(body, item.url);
+ if (variants.length) return variants;
+ if (/#EXTINF/i.test(body)) return [inferQuality(item.url) || { height: 0, label: '原始画质' }];
} catch (_) {}
- }
- return uniqueBy(discovered, function (x) { return x.height; }).sort(function (a, b) { return b.height - a.height; });
+ return [];
+ });
+ return uniqueBy([].concat.apply([], results), function (x) { return x.height; }).sort(function (a, b) { return b.height - a.height; });
}
async function chooseStream(ctx, playlist, requestedHeight, detailUrl) {
- let fallback = null;
- function remember(candidate) { if (!fallback || (candidate.height || 0) > (fallback.height || 0)) fallback = candidate; }
- for (let i = 0; i < Math.min(playlist.length, 4); i++) {
- const item = playlist[i];
- if (!item || !/^https?:\/\//i.test(String(item.url || ''))) continue;
+ const failures = [];
+ const results = await madouMap(nativePlaylist(playlist), 3, async function (item) {
try {
- const body = await fetchHlsText(ctx, item.url, detailUrl);
+ const headers = streamHeaders(ctx, detailUrl, item);
+ const body = await fetchHlsText(ctx, item.url, detailUrl, headers);
const variants = parseMaster(body, item.url).sort(function (a, b) { return b.height - a.height; });
if (!variants.length) {
- if (!/#EXTINF/i.test(body)) continue;
- const inferred = inferQuality(item.url) || { url: item.url, height: 0 };
- inferred.url = item.url; remember(inferred);
- if (requestedHeight && inferred.height === requestedHeight) return inferred;
- } else {
- const ordered = requestedHeight
- ? variants.filter(function (x) { return x.height === requestedHeight; }).concat(variants.filter(function (x) { return x.height !== requestedHeight; })) : variants;
- for (let j = 0; j < ordered.length; j++) {
- if (!requestedHeight && fallback && ordered[j].height <= fallback.height) continue;
- try {
- if (!/#EXTINF/i.test(await fetchHlsText(ctx, ordered[j].url, detailUrl))) continue;
- remember(ordered[j]);
- if (requestedHeight && ordered[j].height === requestedHeight) return ordered[j];
- break;
- } catch (_) {}
- }
+ if (!/#EXTINF/i.test(body)) throw new Error('empty-media-playlist');
+ return Object.assign(inferQuality(item.url) || { height: 0 }, { url: item.url, headers: headers });
}
- } catch (_) {}
- }
- if (fallback) return fallback;
- throw new Error('播放线路检查失败:所有线路均未返回有效的 HLS 媒体清单');
+ const ordered = requestedHeight ? variants.filter(function (q) { return q.height === requestedHeight; }).concat(variants.filter(function (q) { return q.height !== requestedHeight; })) : variants;
+ for (const variant of ordered.slice(0, 6)) {
+ try {
+ if (/#EXTINF/i.test(await fetchHlsText(ctx, variant.url, detailUrl, headers))) return Object.assign(variant, { headers: headers });
+ } catch (_) {}
+ }
+ throw new Error('variant-unavailable');
+ } catch (error) { failures.push(errorMessage(error)); return null; }
+ });
+ const candidates = results.filter(Boolean).sort(function (a, b) { return (b.height || 0) - (a.height || 0); });
+ const selected = requestedHeight && candidates.find(function (q) { return q.height === requestedHeight; });
+ if (selected || candidates.length) return selected || candidates[0];
+ throw new Error('播放线路检查失败:未收到有效 HLS(' + unique(failures).slice(0, 3).join(';') + ');专用网页播放器线路不支持原生交回');
}
-async function fetchHlsText(ctx, url, detailUrl) {
- const body = await fetchText(ctx, url, playbackHeaders(ctx, detailUrl));
- if (!/^\s*#EXTM3U/i.test(body)) throw new Error('HLS 清单无效:' + url);
+async function fetchHlsText(ctx, url, detailUrl, headers) {
+ const body = await fetchText(ctx, url, headers || playbackHeaders(ctx, detailUrl), true);
+ if (!/^\s*#EXTM3U/i.test(body)) throw new Error('HLS 正文无效');
return body;
}
+async function madouMap(items, concurrency, work) {
+ const results = new Array(items.length); let cursor = 0;
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, async function () {
+ while (cursor < items.length) { const index = cursor++; results[index] = await work(items[index], index); }
+ }));
+ return results;
+}
+
function inferQuality(url) {
const value = String(url || '');
let match = value.match(/(?:^|\/)(\d{3,4})x(\d{3,4})(?:\/|$)/i);
@@ -277,7 +279,15 @@ function parseHomeSectionCards(html, title, ctx) {
}
function listURL(ctx, section, page) { return entryURL(ctx) + '/' + String(section.path || 'videos/recent').replace(/^\/+/, '') + (page > 1 ? '/page/' + page : ''); }
-function detailURL(ctx) { const direct = firstNonEmpty(ctx && ctx.detailUrl, ctx && ctx.url); if (/^https?:\/\//i.test(direct)) return direct; const id = firstNonEmpty(ctx && ctx.itemId, ctx && ctx.id, decodeVersionId(ctx && ctx.versionId).slug); return id ? entryURL(ctx) + '/video/cid/' + encodeURIComponent(String(id).replace(/^.*\//, '')) : ''; }
+function detailURL(ctx) {
+ const explicit = firstNonEmpty(ctx && ctx.detailUrl, ctx && ctx.detailURL);
+ if (/^https?:\/\//i.test(explicit)) return explicit;
+ for (const value of [ctx && ctx.itemId, ctx && ctx.id, ctx && ctx.url]) {
+ if (/^https?:\/\//i.test(String(value || '')) && videoSlug(value)) return String(value);
+ }
+ const id = firstNonEmpty(ctx && ctx.itemId, ctx && ctx.id, decodeVersionId(ctx && ctx.versionId).slug);
+ return id && !/^https?:/i.test(id) ? entryURL(ctx) + '/video/cid/' + encodeURIComponent(String(id).replace(/^.*\//, '')) : '';
+}
function entryURL(ctx) { return baseURL(ctx) + MADOU8_ENTRY; }
function baseURL(ctx) { const raw = contextValue(normalizeContext(ctx), 'baseUrl') || MADOU8_DEFAULT_BASE; return String(raw).replace(/\/+$/, ''); }
function categoryAction(s) { return { type: 'category', pageId: s.id, title: s.title, path: s.path, itemAspectRatio: '16:9' }; }
@@ -308,20 +318,83 @@ function attribute(tag, name) { const m = String(tag).match(new RegExp('\\b' + n
function imageHeaders(ctx, referer) { return { Referer: referer || entryURL(ctx) + '/', 'User-Agent': MADOU8_UA }; }
function playbackHeaders(ctx, referer) { return { Referer: referer || entryURL(ctx) + '/', 'User-Agent': MADOU8_UA, Accept: '*/*' }; }
-async function fetchText(ctx, url, extraHeaders) { const response = await httpGet(url, { headers: Object.assign({ 'User-Agent': MADOU8_UA, Accept: 'text/html,application/xhtml+xml,application/json,application/vnd.apple.mpegurl,*/*;q=0.8', Referer: entryURL(ctx) + '/' }, extraHeaders || {}) }); const text = unwrapResponse(response); if (!text) throw new Error('HTTP 返回空内容:' + url); if (/Just a moment|cf-mitigated|Cloudflare Ray ID/i.test(text)) throw new Error('站点返回 Cloudflare 验证页'); return text; }
+async function fetchText(ctx, url, extraHeaders, mediaRequest) {
+ const headers = Object.assign({ 'User-Agent': MADOU8_UA, Accept: 'text/html,application/xhtml+xml,application/json,application/vnd.apple.mpegurl,*/*;q=0.8', Referer: entryURL(ctx) + '/', 'Cache-Control': 'no-cache' }, extraHeaders || {});
+ const response = await madouStage(ctx, mediaRequest ? 'hls-http' : 'page-http', mediaRequest ? 4 : 6, async function () {
+ const r = await httpGet(url, { headers: headers, timeout: mediaRequest ? 4 : 6, timeoutSeconds: mediaRequest ? 4 : 6, browserFallback: false });
+ return { status: Number(r && (r.statusCode || r.status) || 200), text: await unwrapResponse(r) };
+ });
+ let text = response.text;
+ const challenge = /Just a moment|Checking (?:your )?browser|cf-chl-|cf-mitigated|Cloudflare Ray ID/i.test(text);
+ if (challenge && !mediaRequest && !(ctx && ctx.__browserUsed) && typeof Widget !== 'undefined' && Widget.browser && typeof Widget.browser.fetch === 'function') {
+ ctx.__browserUsed = true;
+ text = await madouStage(ctx, 'page-browser', 8, async function () {
+ return await unwrapResponse(await Widget.browser.fetch(url, { visible: false, timeout: 8, timeoutSeconds: 8, waitAfterLoad: 1, headers: headers }));
+ });
+ if (!text || /Just a moment|Checking (?:your )?browser|cf-chl-|cf-mitigated|Cloudflare Ray ID/i.test(text)) throw new Error('站点验证未返回影片内容,stage=page-browser');
+ } else if (response.status >= 400) throw new Error('HTTP ' + response.status + ',stage=' + (mediaRequest ? 'hls-http' : 'page-http'));
+ else if (challenge) throw new Error('站点返回验证页,stage=' + (mediaRequest ? 'hls-http' : 'page-http'));
+ if (!text) throw new Error('HTTP 返回空内容');
+ return text;
+}
+function madouContext(ctx, milliseconds) { return Object.assign({}, normalizeContext(ctx), { __deadline: Date.now() + (milliseconds || 28000), __browserUsed: false }); }
+async function madouStage(ctx, stage, seconds, work) {
+ const limit = Math.min(seconds * 1000, ctx && ctx.__deadline ? ctx.__deadline - Date.now() : seconds * 1000);
+ if (limit <= 0) throw new Error('解析总时限已到,stage=total-deadline');
+ let timer;
+ try { return await Promise.race([Promise.resolve().then(work), new Promise(function (_, reject) { timer = setTimeout(function () { reject(new Error('等待超时,stage=' + stage)); }, limit); })]); }
+ finally { clearTimeout(timer); }
+}
+
async function httpGet(url, options) {
if (typeof Widget !== 'undefined' && Widget && Widget.http) { if (typeof Widget.http.get === 'function') return Widget.http.get(url, options || {}); if (typeof Widget.http.request === 'function') return Widget.http.request(Object.assign({ url: url, method: 'GET' }, options || {})); }
if (typeof $http !== 'undefined' && $http) { if (typeof $http.get === 'function') return $http.get(url, options || {}); if (typeof $http.request === 'function') return $http.request(Object.assign({ url: url, method: 'GET' }, options || {})); }
if (typeof fetch === 'function') return fetch(url, options || {});
throw new Error('当前环境没有可用的 HTTP 客户端');
}
-function unwrapResponse(value) { if (value == null) return ''; if (typeof value === 'string') return value; if (typeof value.text === 'function') return value.text(); if (typeof value.body === 'string') return value.body; if (typeof value.data === 'string') return value.data; if (value.data && typeof value.data.html === 'string') return value.data.html; if (typeof value.text === 'string') return value.text; return JSON.stringify(value.data || value.body || value); }
-function normalizeContext(ctx) { if (typeof ctx === 'string') { try { return JSON.parse(ctx); } catch (_) { return {}; } } return ctx && typeof ctx === 'object' ? ctx : {}; }
+async function unwrapResponse(value) {
+ if (value == null) return '';
+ if (typeof value === 'string') return value;
+ if (typeof value.text === 'function') return String(await value.text());
+ for (const candidate of [value.data, value.body, value.html, value.text]) {
+ if (typeof candidate === 'string') return candidate;
+ if (candidate && typeof candidate.html === 'string') return candidate.html;
+ }
+ return value.data && typeof value.data === 'object' ? JSON.stringify(value.data) : '';
+}
+function normalizeContext(input) {
+ let ctx = input;
+ if (typeof ctx === 'string') { try { ctx = JSON.parse(ctx); } catch (_) { return {}; } }
+ if (!ctx || typeof ctx !== 'object' || Array.isArray(ctx)) return {};
+ const nested = {};
+ for (const name of ['params', 'config', 'settings', 'parameters', 'pagination', 'pageInfo']) {
+ let bag = ctx[name];
+ if (typeof bag === 'string') { try { bag = JSON.parse(bag); } catch (_) { bag = null; } }
+ if (bag && typeof bag === 'object') Object.assign(nested, bag);
+ }
+ return Object.assign(nested, ctx);
+}
+
function contextValue(ctx, key) { ctx = normalizeContext(ctx); const bags = [ctx, ctx.params, ctx.config, ctx.settings, ctx.parameters, ctx.pagination, ctx.pageInfo]; for (let i = 0; i < bags.length; i++) if (bags[i] && bags[i][key] != null && bags[i][key] !== '') return bags[i][key]; return ''; }
function parseJSON(s) { try { return JSON.parse(String(s || '')); } catch (_) { return null; } }
function encodeVersionId(x) { return 'madou8://' + encodeURIComponent(JSON.stringify({ slug: videoSlug(x.url), url: x.url, uid: x.uid, height: x.height || 0 })); }
function decodeVersionId(v) { const s = String(v || ''); if (s.indexOf('madou8://') !== 0) return {}; try { return JSON.parse(decodeURIComponent(s.slice(9))); } catch (_) { return {}; } }
-function resolveURL(value, base) { try { return new URL(value, base).toString(); } catch (_) { if (/^https?:\/\//i.test(value)) return value; return ''; } }
+function resolveURL(value, base) {
+ const input = String(value || '').trim(); if (!input) return '';
+ if (/^https?:\/\//i.test(input)) return input;
+ if (typeof URL === 'function') { try { return new URL(input, base).toString(); } catch (_) {} }
+ const origin = String(base || '').match(/^(https?:)\/\/([^/?#]+)/i); if (!origin) return '';
+ if (input.indexOf('//') === 0) return origin[1] + input;
+ const root = origin[1] + '//' + origin[2];
+ if (input.charAt(0) === '?') return String(base).split(/[?#]/)[0] + input;
+ const suffix = (input.match(/[?#][\s\S]*$/) || [''])[0];
+ const relativePath = input.split(/[?#]/)[0];
+ const path = relativePath.charAt(0) === '/' ? relativePath : String(base).slice(root.length).split(/[?#]/)[0].replace(/[^/]*$/, '') + relativePath;
+ const parts = [];
+ path.split('/').forEach(function (part) { if (part === '..') parts.pop(); else if (part && part !== '.') parts.push(part); });
+ return root + '/' + parts.join('/') + (/\/$/.test(path) ? '/' : '') + suffix;
+}
+
function absoluteURL(ctx, value) { return resolveURL(decodeHTML(value), baseURL(ctx) + '/'); }
function firstMatch(s, re) { const m = String(s || '').match(re); return m ? m[1] || '' : ''; }
function firstNonEmpty() { for (let i = 0; i < arguments.length; i++) if (arguments[i] != null && String(arguments[i]).trim()) return String(arguments[i]).trim(); return ''; }
diff --git a/taolusm-mini-library.js b/taolusm-mini-library.js
index 33a205f..9b14e1d 100644
--- a/taolusm-mini-library.js
+++ b/taolusm-mini-library.js
@@ -9,7 +9,7 @@ const WidgetMetadata = {
id: 'taolusm-mini-library',
name: '套路SM',
title: '套路SM',
- version: '1.0.1',
+ version: '1.0.2',
author: 'Alan huang',
logo: TAOLUSM_LOGO,
icon: TAOLUSM_LOGO,
@@ -176,6 +176,7 @@ async function getDetail(ctx) {
.filter(function (item) { return item.id !== id; })
.slice(0, 18);
const headers = mediaHeaders(ctx);
+ const source = parsePlaybackSource(html, id);
return {
id: id,
@@ -187,8 +188,8 @@ async function getDetail(ctx) {
posterHeaders: headers,
backdropHeaders: headers,
detailImageAspectRatio: '16:9',
- overview: overview,
- resourceGroups: resourceGroupsFor(ctx, id, title),
+ overview: [source.preview ? '公开预览;完整影片需要站点登录及观看权限。' : '', overview].filter(Boolean).join('\n'),
+ resourceGroups: resourceGroupsFor(ctx, id, title, source),
recommendations: recommendations.length ? [
{
id: 'related',
@@ -200,26 +201,44 @@ async function getDetail(ctx) {
};
}
-function getResourceVersions(ctx) {
+async function getResourceVersions(ctx) {
ctx = normalizeRuntimeContext(ctx);
const id = itemIdFromContext(ctx);
if (!id) return [];
- return resourceGroupsFor(ctx, id, stringValue(ctx && ctx.title) || '在线播放');
+ const html = await fetchText(ctx, baseURL(ctx) + '/v/' + encodeURIComponent(id));
+ const source = parsePlaybackSource(html, id);
+ if (!source.url) throw new Error('套路SM 当前页面未提供可播放的影片,可能需要登录及观看权限');
+ return resourceGroupsFor(ctx, id, stringValue(ctx.title) || '在线播放', source);
}
-function resolvePlayback(ctx) {
+async function resolvePlayback(ctx) {
ctx = normalizeRuntimeContext(ctx);
const id = itemIdFromContext(ctx);
- const url = stringValue(ctx && ctx.url) || (id ? downloadURL(ctx, id) : '');
- if (!url) throw new Error('套路SM 播放参数无效');
+ if (!id) throw new Error('套路SM 播放参数无效');
+ const html = await fetchText(ctx, baseURL(ctx) + '/v/' + encodeURIComponent(id));
+ const source = parsePlaybackSource(html, id);
+ if (!source.url) throw new Error('套路SM 当前页面未提供媒体地址;完整影片需站点登录及观看权限');
+ if (source.preview && (/^download-/.test(stringValue(ctx.versionId)) || /\/download\//.test(stringValue(ctx.url)))) {
+ throw new Error('旧下载线路需要站点登录及观看权限;当前公开页面仅提供预览,请重新打开详情并选择“公开预览”');
+ }
+ const hls = /\.m3u8(?:$|[?#])/i.test(source.url);
+ if (hls) {
+ const body = await fetchText(ctx, source.url);
+ if (!/^\s*#EXTM3U/i.test(body)) throw new Error('套路SM 当前播放线路没有返回有效 HLS');
+ }
+ return { url: source.url, container: hls ? 'm3u8' : 'mp4', headers: mediaHeaders(ctx), startPositionSeconds: 0, isLive: false, streamKind: hls ? 'hls' : 'file' };
+}
- return {
- url: url,
- container: 'mp4',
- headers: mediaHeaders(ctx),
- isLive: false,
- streamKind: 'file'
- };
+function parsePlaybackSource(html, id) {
+ const scripts = String(html || '').match(/`;
+
+test('KBJ unavailable home reports the source failure instead of successful empty media', async () => {
+ const c = load('kbjfan-mini-library.js'); c.Widget.http.get = async () => { throw new Error('connection closed'); };
+ await assert.rejects(c.getHome({}), /KBJFan 加载失败.*connection closed/);
+ const section = await c.getHomeSection({ sectionId: 'dance' }); assert.equal(section.items.length, 0); assert.match(section.error, /connection closed/);
+});
+test('KBJ rejects a repurposed domain redirect and does not invent a replacement', async () => {
+ const c = load('kbjfan-mini-library.js'); c.Widget.http.get = async () => ({ statusCode: 200, finalURL: 'https://other.test/', data: 'Other site
' });
+ await assert.rejects(c.getHome({}), /跳转到其他站点/);
+ c.Widget.http.get = async () => ({ statusCode: 403, data: 'Forbidden' });
+ await assert.rejects(c.getHome({}), /HTTP 403/);
+});
+test('KBJ accepts request-only HTTP and nested response HTML while preserving old contexts', async () => {
+ const c = load('kbjfan-mini-library.js'); const calls = [];
+ c.Widget.http = { async request(options) { calls.push(options); return { statusCode: 200, body: { html: '
' } }; } };
+ const home = await c.getHome(JSON.stringify({ params: JSON.stringify({ baseURL: 'https://kbj.test' }) }));
+ assert.equal(home.hero[0].id, 'https://kbj.test/film/'); assert.equal(calls[0].url, 'https://kbj.test/koreanbjdance/'); assert.equal(calls[0].browserFallback, false);
+});
+test('KBJ and Taolu bound a hung response body as well as their HTTP request', async () => {
+ for (const file of ['kbjfan-mini-library.js', 'taolusm-mini-library.js']) {
+ const c = load(file, { setTimeout: (fn, ms) => setTimeout(fn, Math.min(ms, 15)) });
+ c.Widget.http.get = async () => ({ statusCode: 200, text: () => new Promise(() => {}) });
+ await assert.rejects(c.fetchText({}, 'https://source.test/'), /超时/);
+ }
+});
+test('Madou first screen loads only recent media; category artwork remains lazy', async () => {
+ const c = load('madou8-mini-library 5.js'); const calls = [];
+ c.Widget.http.get = async url => { calls.push(url); return { statusCode: 200, body: { html: madouCard('first') } }; };
+ const home = await c.getHome(JSON.stringify({ params: JSON.stringify({ baseUrl: 'https://madou.test' }) }));
+ assert.deepEqual(calls, ['https://madou.test/asian/zh-CN/videos/recent']); assert.equal(home.hero[0].id, 'first');
+ assert.equal(home.sections[0].lazy, true); assert.equal(home.sections[1].items[0].rank, 1);
+});
+test('Madou page failures do not turn into successful empty home or fake qualities', async () => {
+ const c = load('madou8-mini-library 5.js'); c.Widget.http.get = async () => ({ statusCode: 403, data: 'Forbidden
' });
+ await assert.rejects(c.getHome({}), /HTTP 403/);
+ c.Widget.http.get = async () => ({ statusCode: 200, data: 'No cards' });
+ await assert.rejects(c.getHome({}), /未收到影片列表/);
+ c.fetchStreamInfo = async () => ({ playlist: [{ url: 'https://invalid.test/stream.m3u8' }] }); c.fetchHlsText = async () => { throw new Error('HTTP 403'); };
+ await assert.rejects(c.buildPlaybackGroups({}, 'https://madou.test/detail', 'uid', 'Film'), /没有可验证/);
+});
+test('Madou one challenged page can return native browser HTML without media capture or retry stacks', async () => {
+ const c = load('madou8-mini-library 5.js'); let browsers = 0;
+ c.Widget.http.get = async () => ({ statusCode: 403, data: 'Just a moment' });
+ c.Widget.browser = { async fetch(url, options) { browsers++; assert.equal(options.visible, false); assert.equal(options.waitForMediaSource, undefined); return { html: madouCard('verified') }; } };
+ const home = await c.getHome({}); assert.equal(home.hero[0].id, 'verified'); assert.equal(browsers, 1);
+ await assert.rejects(c.fetchHlsText({}, 'https://cdn.test/master.m3u8', 'https://madou.test/'), /HTTP 403/); assert.equal(browsers, 1);
+});
+test('Madou relative cards and signed HLS paths round-trip without URL global', () => {
+ const c = load('madou8-mini-library 5.js', { URL: undefined });
+ const items = c.parseCards(madouCard('film'), {});
+ assert.equal(items[0].action.detailUrl, 'https://madou8.pw/asian/zh-CN/video/cid/film');
+ assert.equal(c.resolveURL('../720/video.m3u8?sig=a/b#part', 'https://cdn.test/token/master.m3u8'), 'https://cdn.test/720/video.m3u8?sig=a/b#part');
+});
+test('Madou evaluates a working line beyond the old first-four limit and skips web-only schemes', async () => {
+ const c = load('madou8-mini-library 5.js'); const calls = [];
+ const playlist = Array.from({length: 5}, (_, i) => ({ url: `https://cdn.test/${i}/master.m3u8` }));
+ playlist.unshift({ url: 'https://enc.test/stream.m3u8', playMode: 'streampipe' }, { url: 'https://web.test/stream.m3u8', native: false });
+ c.fetchHlsText = async (ctx, url) => { calls.push(url); if (url.includes('/4/')) return url.endsWith('master.m3u8') ? master : media; throw new Error('HTTP 403'); };
+ const chosen = await c.chooseStream({}, playlist, 1080, 'https://madou.test/detail');
+ assert.equal(chosen.height, 1080); assert.match(chosen.url, /\/4\/1080.m3u8/); assert.ok(!calls.some(url => /enc.test|web.test/.test(url)));
+});
+test('Madou retains public player Accept-sign headers for master, variant and native playback', async () => {
+ const c = load('madou8-mini-library 5.js'); const calls = [];
+ c.Widget.http.get = async (url, options) => { calls.push({url, headers: options.headers}); return {statusCode:200,data:url.endsWith('master.m3u8') ? master : media}; };
+ const chosen = await c.chooseStream({}, [{url:'https://cdn.test/master.m3u8',urlSign:'opaque normal value'}], 720, 'https://madou.test/detail');
+ assert.equal(chosen.height, 720); assert.equal(chosen.headers.Accept, '*/*;sign=opaque%20normal%20value');
+ assert.ok(calls.every(x => x.headers.Accept === chosen.headers.Accept)); assert.equal(chosen.headers.Origin, undefined);
+ const q = await c.discoverVariants({}, [{url:'https://cdn.test/master.m3u8',urlSign:'private-now'}], 'https://madou.test/detail');
+ assert.ok(!JSON.stringify(q).includes('private-now'));
+});
+test('Madou original media playlists expose honest original quality and refresh before every play', async () => {
+ const c = load('madou8-mini-library 5.js'); let api = 0;
+ c.Widget.http.get = async url => ({statusCode:200,data:url.includes('/api/video/stream') ? JSON.stringify({playlist:[{url:`https://cdn.test/stream.m3u8?fresh=${++api}`,urlSign:`now-${api}`} ]}) : media});
+ const detail = 'https://madou8.pw/asian/zh-CN/video/cid/film';
+ const groups = await c.buildPlaybackGroups(c.madouContext({}), detail, 'uid', 'Film');
+ assert.equal(groups[0].versions[0].name, '原始画质'); assert.ok(!JSON.stringify(groups).includes('now-'));
+ const action = groups[0].versions[0].action;
+ for (const height of [0, 720, 0]) {
+ const result = await c.resolvePlayback(JSON.stringify({...action,qualityId:height,url:'https://expired.test/file.m3u8'}));
+ assert.match(result.url, new RegExp(`fresh=${api}$`)); assert.equal(result.headers.Accept, `*/*;sign=now-${api}`);
+ }
+ assert.equal(api, 4);
+});
+test('Madou media discovery caps concurrency and the overall deadline when native requests hang', async () => {
+ const c = load('madou8-mini-library 5.js', {setTimeout:(fn,ms)=>setTimeout(fn,Math.min(ms,20))}); let active=0,max=0;
+ c.Widget.http.get = async () => { active++; max=Math.max(max,active); return new Promise(()=>{}); };
+ const playlist=Array.from({length:9},(_,i)=>({url:`https://cdn.test/${i}/master.m3u8`}));
+ const start=Date.now(); await assert.rejects(c.chooseStream({__deadline:Date.now()+25},playlist,0,'https://madou.test/detail'), /stage=/);
+ assert.ok(Date.now()-start<150); assert.ok(max<=6); // timed-out native calls cannot be cancelled; logical workers remain three
+});
+test('Taolu detail and versions identify public previews, not download/login pages or recommendations', async () => {
+ const c=load('taolusm-mini-library.js'); const calls=[];
+ c.Widget.http.get=async url=>{calls.push(url);return {statusCode:200,data:taoluPage('121576','https://tl.test/preview_mp4/121576.mp4')};};
+ const detail=await c.getDetail({itemId:'121576'}); const version=detail.resourceGroups[0].versions[0];
+ assert.equal(version.name,'公开预览(非完整影片)'); assert.equal(version.url,undefined); assert.match(detail.overview,/完整影片需要/);
+ const result=await c.resolvePlayback(JSON.stringify(version.action)); assert.equal(result.url,'https://tl.test/preview_mp4/121576.mp4'); assert.equal(result.container,'mp4'); assert.equal(result.headers.Origin,undefined);
+ assert.ok(calls.every(url=>url.endsWith('/v/121576'))); assert.equal(c.parsePlaybackSource(taoluPage('other','https://tl.test/preview_mp4/other.mp4'),'121576').url,'');
+});
+test('Taolu old complete/download choices report access requirements instead of silently substituting previews', async () => {
+ const c=load('taolusm-mini-library.js');c.Widget.http.get=async()=>({statusCode:200,data:taoluPage('121576','https://tl.test/preview_mp4/121576.mp4')});
+ await assert.rejects(c.resolvePlayback({versionId:'download-121576'}),/旧下载线路需要/);
+ await assert.rejects(c.resolvePlayback({url:'https://taolusm.com/download/121576'}),/旧下载线路需要/);
+ assert.equal(c.itemIdFromContext({itemId:'https://taolusm.com/v/121576?year=2026'}),'121576');
+});
+test('Taolu missing source, HTTP errors and login redirects never become MP4 playback results', async () => {
+ const c=load('taolusm-mini-library.js');
+ for(const response of [{statusCode:200,data:''},{statusCode:403,data:'Forbidden'},{statusCode:200,finalURL:'https://taolusm.com/login',data:'Login'}]) {
+ c.Widget.http.get=async()=>response;
+ await assert.rejects(c.resolvePlayback({itemId:'121576'}),/未提供|HTTP 403|需要站点登录/);
+ }
+});
+
+test('Madou history refresh ignores an old media URL when only stable item/version is present', async () => {
+ const c=load('madou8-mini-library 5.js');const version=c.encodeVersionId({url:'https://madou8.pw/asian/zh-CN/video/cid/film',uid:'uid',height:720});
+ assert.equal(c.detailURL({itemId:'film',url:'https://expired.test/stream.m3u8'}),'https://madou8.pw/asian/zh-CN/video/cid/film');
+ assert.equal(c.detailURL({versionId:version,url:'https://expired.test/stream.m3u8'}),'https://madou8.pw/asian/zh-CN/video/cid/film');
+});
+test('Madou native response body timeouts and logical media concurrency remain bounded', async () => {
+ const c=load('madou8-mini-library 5.js',{setTimeout:(fn,ms)=>setTimeout(fn,Math.min(ms,15))});
+ c.Widget.http.get=async()=>({statusCode:200,text:()=>new Promise(()=>{})});
+ await assert.rejects(c.getHome({}),/stage=page-http/);
+ let active=0,max=0;
+ const results=await c.madouMap([1,2,3,4,5,6,7],3,async value=>{active++;max=Math.max(max,active);await new Promise(resolve=>setTimeout(resolve,3));active--;return value;});
+ assert.deepEqual(plain(results),[1,2,3,4,5,6,7]);assert.equal(max,3);
+});
+test('KBJ unrelated HTML cannot create fake playable lines or successful first categories', async () => {
+ const c=load('kbjfan-mini-library.js');c.Widget.http.get=async()=>({statusCode:200,data:'Other site'});
+ await assert.rejects(c.getCategory({pageId:'dance'}),/没有原站影片/);
+ const detail=await c.getDetail({itemId:'https://www.kbjfan.com/old-film/'});assert.equal(detail.resourceGroups.length,0);
+ await assert.rejects(c.getResourceVersions({itemId:'https://www.kbjfan.com/old-film/'}),/没有可播放媒体/);
+});
From 1e13e8c8c65270f29a65af0d20be9643228d7e23 Mon Sep 17 00:00:00 2001
From: hzl <1803573449@qq.com>
Date: Sat, 3 Oct 2026 22:50:00 +0800
Subject: [PATCH 6/6] Document approved library update and source limitations
---
CHANGELOG.md | 6 +++---
README.md | 12 ++++++------
2 files changed, 9 insertions(+), 9 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index d9018fa..5eef9a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,8 +1,8 @@
-# 统一故障修复稿 — 2026-10-03
+# 统一故障修复 — 2026-10-03
整合 MissAV 1.5.11、ASMRLIB 1.2.2、JAVGG 1.0.2、SexBJCam 1.1.9、麻豆亚洲版 1.1.2、套路 SM 1.0.2、KBJ fan 1.0.2,原下载路径全部保留。整体 103 项媒体库回归及仓库 CI 对应检查通过;下文各独立修复的检查数是各自主分支基线的验证记录。
-当前稿件仍有明确限制:KBJ 原站内容未恢复;套路 SM 完整影片仍需正常权限;麻豆 IPX-559 只有有效原始 HLS 的证据,1080P 及用户报告所属源待确认。全部 iPhone 原生显示/播放验收待设备确认。本稿保持草稿,不更改默认分支。
+当前版本仍有明确限制:KBJ 原站内容未恢复;套路 SM 完整影片仍需正常权限;麻豆 IPX-559 只有有效原始 HLS 的证据,1080P 及用户报告所属源待确认。全部 iPhone 原生显示/播放验收待设备确认。用户已要求将本轮改动发布至原 GitHub 下载入口,并在 iPhone 更新测试。
# 麻豆 / 套路 SM / KBJ fan — 2026-10-03
@@ -10,7 +10,7 @@
- 普通 HLS 候选由前四条扩大到最多 12 条,限三个逻辑并发和 28 秒总预算;原生调用超时后无法由 JS 强制取消。读取并正常转交 API/公开播放器使用的 `Accept` 凭据,播放时刷新,资源版本不持有媒体凭据或签名 URL。网页专用线路不冒充原生 HLS;未知分辨率显示原始画质,全部失败时不返回假默认线路。
- 桌面已验证五个分类的两页、搜索两页、三部详情/清单/分片。麻豆 IPX-559 返回有效原始 HLS,但该线路没有分辨率证据,1080P 仍未确认;其他 CDN 仍有 403。另一部影片完成 1080P → 720P → 1080P 的刷新验证。
- 套路 SM 1.0.2 读取对应影片主播放器,不把下载/登录 HTML 当 MP4,不误取推荐区媒体。真实匿名样本均只提供预览,资源与说明明确标注;旧下载版本仍能识别,权限不足时报明原因。完整影片需要站点正常登录及权限,未宣称修复该限制。
-- KBJ fan 1.0.2 对连接、HTTP 状态、异常跳转、空响应及没有原站影片作明确诊断,HTTP 请求及正文限制为 8 秒。原域名当前连接异常/跳转其他站点,没有确认到替代站点,因此此项为诊断改进,内容未恢复。
+- KBJ fan 1.0.2 对连接、HTTP 状态、异常跳转、空响应及没有原站影片作明确诊断,HTTP 请求及正文限制为 8 秒。原域名当前连接异常/跳转其他站点,用户 iPhone 测试也确认打不开。没有确认到替代站点,因此此项为诊断改进,内容未恢复。
- 保留三个脚本原下载路径、媒体库 ID、原参数名及历史稳定标识。64 项独立媒体库回归(新增 18 项)与仓库 CI 对应检查通过;iPhone 实际显示和原生播放仍待验收。
# JAVGG 1.0.2 — 2026-10-03
diff --git a/README.md b/README.md
index c5339f6..122fe56 100644
--- a/README.md
+++ b/README.md
@@ -50,7 +50,7 @@ GitHub Actions 会先生成兼容副本并验证;`main` 的检查通过后,
| [madou8-domestic-mini-library.js](madou8-domestic-mini-library.js) | 1.2.1 | 1.2.2 |
| [madou8-mini-library 5.js](madou8-mini-library%205.js) | 1.1.0 | 1.1.2 |
| [manko-fun-mini-library.js](manko-fun-mini-library.js) | 1.8.0 | 1.8.1 |
-| [missav-mini-library-download-working 6.js](missav-mini-library-download-working%206.js) | 1.5.9 | 1.5.10 |
+| [missav-mini-library-download-working 6.js](missav-mini-library-download-working%206.js) | 1.5.9 | 1.5.11 |
| [novipnoad-mini-library.js](novipnoad-mini-library.js) | 1.0.0 | 1.0.1 |
| [pornhub.media-library.js](pornhub.media-library.js) | 1.0.0 | 1.0.1 |
| [pornhub_int.js](pornhub_int.js) | 1.1.5 | 1.1.6 |
@@ -61,19 +61,19 @@ GitHub Actions 会先生成兼容副本并验证;`main` 的检查通过后,
具体行为和设备验证限制见 [CHANGELOG.md](CHANGELOG.md)。
-## 统一故障修复稿 — 2026-10-03
+## 统一故障修复 — 2026-10-03
- 麻豆亚洲版 `madou8-mini-library 5.js` 为 1.1.2:首屏只取最近更新,分类预览随后加载;HTTP 错误及验证失败明确报告。五个分类前两页各返回 12 条且没有跨页重复;桌面首屏 1.543 秒。原文件名、ID、`baseUrl` 和 `madou8://` 版本载荷保持兼容。
- 播放检查最多 12 条实际普通 HLS 线路,使用三个逻辑并发任务和 28 秒预算,避免接口顺序变化时漏掉第五条以后的有效源。沿用网页 `xhrSetup` 的 `Accept` 凭据,每次从 API 刷新,不写入版本/缓存。专用 StreamPipe/service-worker 线路及明确 `native:false` 的线路不会伪装成原生地址。
- 麻豆 IPX-559 已验证原始 HLS 及 MPEG-TS 分片;来源未标明该线路分辨率,没有宣称 1080P 恢复。STCV-497 返回 720P;另一部普通影片返回 1080P/720P/480P/360P,并通过 1080P → 720P → 1080P 的桌面请求。仍有 CDN 返回 403;未通过伪造权限或签名规避。
- 套路 SM `taolusm-mini-library.js` 为 1.0.2:旧下载入口会跳到登录页,现从当前影片主播放器配置读取实际源。三部匿名 HTTP 样本仅提供公开预览,资源明确标注“公开预览(非完整影片)”;旧下载选择报告登录/观看权限,完整影片未宣称恢复。
-- KBJ fan `kbjfan-mini-library.js` 为 1.0.2:补充 HTTP 状态、响应正文、8 秒截止时间和域名跳转诊断,加载失败不再返回成功的空首页或假播放线路。当前原域名连接中断,另一次网页读取跳到其他站点;没有确认到官方替代地址,内容恢复仍待可用站点。
+- KBJ fan `kbjfan-mini-library.js` 为 1.0.2:补充 HTTP 状态、响应正文、8 秒截止时间和域名跳转诊断,加载失败不再返回成功的空首页或假播放线路。当前原域名连接中断,另一次网页读取跳到其他站点;用户 iPhone 测试也确认打不开。没有确认到官方替代地址,内容恢复仍待可用站点。
-三个新增修复原先独立通过 64 项回归;与 MissAV、ASMRLIB、JAVGG、SexBJCam 整合后共 103 项媒体库回归及仓库检查通过。以上真实媒体结果来自桌面 HTTP;DreamBy/iPhone 的显示、原生起播及验证状态衔接仍需设备确认。用户报告的 IPX-559 所属媒体库尚待确认,本轮按麻豆亚洲版检查该影片。原下载路径全部保留;本分支为等待统一确认的修复稿,整合入口为 [PR #13](https://github.com/bbnotcode/ph_js/pull/13),尚未合并默认分支。
+三个新增修复原先独立通过 64 项回归;与 MissAV、ASMRLIB、JAVGG、SexBJCam 整合后共 103 项媒体库回归及仓库检查通过。以上真实媒体结果来自桌面 HTTP;DreamBy/iPhone 的显示、原生起播及验证状态衔接仍需设备确认。用户报告的 IPX-559 所属媒体库尚待确认,本轮按麻豆亚洲版检查该影片。原下载路径全部保留;完整变更见 [PR #13](https://github.com/bbnotcode/ph_js/pull/13)。用户已要求发布这些改动,更新时沿用既有导入地址;无需删除媒体库重导。
ASMRLIB 1.2.2 的 BI/AB 使用“验证后播放”入口,在原详情页内选择线路并手动验证或点击播放。源设置中的“显示播放验证页面”默认开启。只有浏览器返回最终媒体地址才会交给原生播放器;若只返回网页或 blob,脚本会显示具体阶段。此流程的代码及模拟已验证,iPhone 嵌套媒体回传、自动交回和起播仍待设备确认。
-## JAVGG 1.0.2 播放修复稿
+## JAVGG 1.0.2 播放修复
`javgg-mini-library.js` 保持原下载路径、媒体库 ID、`baseUrl` 参数和旧 `javgg://` 载荷。按实际 `data-nume` 对应 VH、playmate、luluvdoo、SW 线路,静态读取公开播放器的打包代码,不执行远程脚本。资源列表仅包含本次成功读取到的真实画质,最高画质默认;地址在每次播放时刷新。
@@ -81,7 +81,7 @@ ASMRLIB 1.2.2 的 BI/AB 使用“验证后播放”入口,在原详情页内
匿名桌面 HTTP 已验证 CAWB-046 的 luluvdoo 1080p、SW 1080p/720p/480p 和 MIRD-287 的 luluvdoo 720p,master、变体与分片可请求;ATID-666 的 SW 清单当时返回 404。首页/详情 HTTP 还可能遇到站点浏览器检查,浏览器正常打开不等于设备脚本能读取。DreamBy 浏览器回传、iPhone 原生起播与切画质仍待实测。
-## SexBJCam 1.1.9 播放修复稿
+## SexBJCam 1.1.9 播放修复
保留 `sexbjcam-mini-library.js` 原下载路径、`sexbjcam-mini-library` ID、`baseURL` 参数、历史详情 URL 与 `quality:1080/720/480` ID。播放器改为先用 HTTP 读取公开打包配置,必要时只做一次 12 秒浏览器媒体捕获;HLS 正文使用最多 4 秒 HTTP 请求,不再用浏览器导航清单。详情、资源发现和播放各有 28 秒总时限;脚本截止时间不能强制取消宿主原生任务。