From a393f687877e5190b33475f2fef0634f45c63968 Mon Sep 17 00:00:00 2001 From: hzl <1803573449@qq.com> Date: Sat, 3 Oct 2026 21:15:45 +0800 Subject: [PATCH] Preserve ASMRLIB embedding and allow manual playback verification --- CHANGELOG.md | 9 +++ README.md | 4 +- asmrlib-mini-library.js | 168 ++++++++++++++++++++++++---------------- tests/adapters.test.cjs | 68 ++++++++++++++++ 4 files changed, 181 insertions(+), 68 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 552a6bf..d9018f6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # 2026-10-03 +## ASMRLIB 1.2.2 播放上下文修复稿 + +- 三个实际样本当前均提供 BI/AB。BI 嵌入页要求手动点击真人验证;AB 的公开脚本在 `top.location == self.location` 时跳转到托管站首页。原来的两次隐藏独立播放器请求不具备正确交互和嵌入上下文。 +- BI/AB 改为一个“验证后播放”资源入口,在原 ASMRLIB 详情页内保留 iframe 和线路选择。新增 `browserVisible` 参数,默认显示页面,验证和播放点击由用户完成;旧条目 ID 和旧 BI/AB 版本载荷仍能进入同一影片的详情页。 +- 仅使用一次 20 秒浏览器媒体等待,保留旧 UP 的最多 5 秒 API 尝试及一次页面回退。去掉未经当前宿主证实的 capture 标志,不再隐藏尝试主备各 18 秒。脚本侧截止时间不能强制取消原生浏览器任务。 +- 支持 `capturedRequests` 媒体返回;优先使用捕获媒体自己的请求头。若未返回媒体请求头,只保留 User-Agent,不把外层页面 Origin/Referer 推定为视频 CDN 的请求头。 +- 缺少媒体、blob-only、验证未完成、宿主异常和请求超时分别报告阶段及有限返回字段名;不回传原始异常、Cookie 或签名 URL 到错误信息。 +- 55 项媒体库回归通过(新增 9 项 ASMRLIB 流程检查);语法、39 个媒体库契约通过。未完成 DreamBy iPhone 可见验证、嵌套 iframe 媒体回传和原生起播;本稿不能据此宣称所有影片已恢复播放。 + 基于 `main` 的 `8e43017532ec81e3338046ba6e32dff7655bd3dd` 修复审查中 O01–O25;保留所有已发布文件路径。 ## 加载与失败恢复 diff --git a/README.md b/README.md index 7754c84..9fff2e7 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ GitHub Actions 会先生成兼容副本并验证;`main` 的检查通过后, | [591av-mini-library.js](591av-mini-library.js) | 1.3.1 | 1.3.2 | | [91porny_int.js](91porny_int.js) | 0.9.6 | 0.9.7 | | [MissAV 3.0.js](MissAV%203.0.js) | 3.0 | 3.0.1 | -| [asmrlib-mini-library.js](asmrlib-mini-library.js) | 1.2.0 | 1.2.1 | +| [asmrlib-mini-library.js](asmrlib-mini-library.js) | 1.2.0 | 1.2.2 | | [girigirilove-mini-library.js](girigirilove-mini-library.js) | 1.0.0 | 1.0.1 | | [jable.js](jable.js) | 1.3.0 | 1.3.1 | | [jable.media-library.js](jable.media-library.js) | 1.0.0 | 1.0.1 | @@ -59,6 +59,8 @@ GitHub Actions 会先生成兼容副本并验证;`main` 的检查通过后, 具体行为和设备验证限制见 [CHANGELOG.md](CHANGELOG.md)。 +ASMRLIB 1.2.2 的 BI/AB 使用“验证后播放”入口,在原详情页内选择线路并手动验证或点击播放。源设置中的“显示播放验证页面”默认开启。只有浏览器返回最终媒体地址才会交给原生播放器;若只返回网页或 blob,脚本会显示具体阶段。此流程的代码及模拟已验证,iPhone 嵌套媒体回传、自动交回和起播仍待设备确认。 + ## 本地验收 ```bash diff --git a/asmrlib-mini-library.js b/asmrlib-mini-library.js index 7537e6f..1ebe3d5 100644 --- a/asmrlib-mini-library.js +++ b/asmrlib-mini-library.js @@ -2,7 +2,7 @@ * ASMRLIB - Dreamby / baiPlay 自定义媒体库 * Source: https://asmrlib.com/ * @author Alan huang - * @version 1.2.0 + * @version 1.2.2 */ const ASMRLIB_BASE = 'https://asmrlib.com'; @@ -89,16 +89,17 @@ function asmrResponseText(response) { return ''; } -async function asmrHTTP(url, referer) { +async function asmrHTTP(url, referer, timeout) { const headers = asmrHeaders(referer); + timeout = timeout || 30; let response; if (typeof Widget !== 'undefined' && Widget.http) { - if (typeof Widget.http.get === 'function') response = await Widget.http.get(url, { headers: headers, timeout: 30 }); - else if (typeof Widget.http.request === 'function') response = await Widget.http.request({ url: url, method: 'GET', headers: headers, timeout: 30 }); + if (typeof Widget.http.get === 'function') response = await Widget.http.get(url, { headers: headers, timeout: timeout, timeoutSeconds: timeout }); + else if (typeof Widget.http.request === 'function') response = await Widget.http.request({ url: url, method: 'GET', headers: headers, timeout: timeout, timeoutSeconds: timeout }); } if (response === undefined && typeof $http !== 'undefined' && $http) { - if (typeof $http.get === 'function') response = await $http.get(url, { headers: headers, timeout: 30 }); - else if (typeof $http.request === 'function') response = await $http.request({ url: url, method: 'GET', headers: headers, timeout: 30 }); + if (typeof $http.get === 'function') response = await $http.get(url, { headers: headers, timeout: timeout, timeoutSeconds: timeout }); + else if (typeof $http.request === 'function') response = await $http.request({ url: url, method: 'GET', headers: headers, timeout: timeout, timeoutSeconds: timeout }); } if (response === undefined && typeof fetch === 'function') { const nativeResponse = await fetch(url, { headers: headers }); @@ -112,7 +113,7 @@ async function asmrHTTP(url, referer) { function asmrId(raw) { const value = asmrText(raw); - const match = value.match(/(?:asmrlib:\/\/post\/|\/posts\/)([a-f0-9]{32})/i); + const match = value.match(/(?:asmrlib(?:-page)?:\/\/post\/|\/posts\/)([a-f0-9]{32})/i); return match ? match[1] : (/^[a-f0-9]{32}$/i.test(value) ? value : ''); } function asmrPayload(id) { return 'asmrlib://post/' + asmrId(id); } @@ -191,12 +192,16 @@ function getManifest() { id: 'asmrlib', name: 'ASMRLIB', title: 'ASMRLIB', - version: '1.2.1', + version: '1.2.2', author: 'Alan huang', logo: ASMRLIB_LOGO, icon: ASMRLIB_LOGO, capabilities: { search: true, aggregation: true, playbackHistory: true }, - aggregation: { search: true, playbackHistory: true } + aggregation: { search: true, playbackHistory: true }, + parameters: [{ + name: 'browserVisible', title: '显示播放验证页面', type: 'boolean', defaultValue: true, + description: 'BI 需要手动验证;AB 必须在详情页内打开。请在页面里选择线路并点击验证或播放,捕获到媒体后返回播放器。' + }] }; } @@ -291,6 +296,21 @@ function asmrDetailData(html, id) { } function asmrResourceGroups(detail) { + const embedded = detail.versions.some(function (version) { + return /^https?:\/\/(?:bysetayico\.com|abyssplayer\.com)(?:\/|$)/i.test(version.playerUrl); + }); + if (embedded) { + // These players require the site's iframe/interaction context. Present one + // page entry; the user chooses BI/AB inside that page instead of promising + // that a standalone hidden request can select either embedded player. + return [{ id: 'online', title: '在线播放', versions: [{ + id: 'asmrlib-page://post/' + detail.id, + name: '验证后播放(BI / AB)', + subtitle: '在页面里选择 BI 或 AB,再点击验证或播放', + default: true, + action: { type: 'play', itemId: asmrPayload(detail.id), versionId: 'asmrlib-page://post/' + detail.id, title: detail.title } + }] }]; + } return [{ id: 'online', title: '在线播放', @@ -333,21 +353,38 @@ function asmrPlayable(value) { return /^https?:\/\//i.test(asmrText(value)) && /\.(?:m3u8|mp4|mpd|m4v|mov|webm)(?:[?#]|$)/i.test(asmrText(value)); } function asmrBrowserMedia(result) { - if (!result) return ''; + return asmrCapturedMedia(result).url; +} +function asmrCapturedMedia(result) { + if (!result) return { url: '', headers: {} }; + const candidates = []; + const add = function (value, headers) { + if (asmrPlayable(value) && !/\/(?:ads?|advert|preview|trailer)(?:[\/_.-]|$)/i.test(value)) candidates.push({ url: value, headers: asmrMediaHeaders(headers) }); + }; const directKeys = ['url', 'mediaURL', 'mediaUrl', 'videoURL', 'videoUrl', 'playURL', 'playUrl', 'src']; - for (let i = 0; i < directKeys.length; i += 1) if (asmrPlayable(result[directKeys[i]])) return result[directKeys[i]]; - const arrays = [result.mediaSources, result.mediaRequests, result.requests, result.responses, result.urls]; + for (let i = 0; i < directKeys.length; i += 1) add(result[directKeys[i]], result.mediaHeaders || result.requestHeaders); + const arrays = [result.mediaSources, result.mediaRequests, result.capturedRequests, result.requests, result.responses, result.urls]; for (let a = 0; a < arrays.length; a += 1) { if (!Array.isArray(arrays[a])) continue; for (let i = 0; i < arrays[a].length; i += 1) { const entry = arrays[a][i]; const value = typeof entry === 'string' ? entry : entry && (entry.url || entry.src || entry.responseURL); - if (asmrPlayable(value) && !/^blob:/i.test(value)) return value; + add(value, entry && (entry.requestHeaders || entry.headers)); } } + if (candidates.length) return candidates[0]; const text = asmrResponseText(result).replace(/\\\//g, '/'); const match = text.match(/https?:\/\/[^\s"'<>\\]+\.(?:m3u8|mp4|mpd|m4v|mov|webm)(?:\?[^\s"'<>\\]*)?/i); - return match && asmrPlayable(match[0]) ? match[0] : ''; + if (match) add(match[0], null); + return candidates[0] || { url: '', headers: {} }; +} +function asmrMediaHeaders(headers) { + const output = {}; + Object.keys(headers || {}).forEach(function (key) { + const canonical = { referer: 'Referer', origin: 'Origin', 'user-agent': 'User-Agent', cookie: 'Cookie' }[key.toLowerCase()]; + if (canonical && typeof headers[key] === 'string') output[canonical] = headers[key]; + }); + return output; } function asmrVersionPlayerURL(input) { const versionId = asmrText(asmrPick(input, ['versionId', 'resourceId', 'lineId'], '')); @@ -386,7 +423,7 @@ async function asmrResolveUP(playerUrl) { if (!code) return ''; const api = 'https://v.upn.one/api/v1/video?id=' + encodeURIComponent(code) + '&w=390&h=844&r=' + encodeURIComponent(ASMRLIB_BASE); - const encrypted = await asmrHTTP(api, ASMRLIB_BASE + '/'); + const encrypted = await asmrHTTP(api, ASMRLIB_BASE + '/', 5); const data = asmrDecryptUP(encrypted); const candidates = [data.source, data.cfNative]; if (data.hlsVideoTiktok && data.streamingConfig) { @@ -416,69 +453,66 @@ async function asmrResolveUP(playerUrl) { async function resolvePlayback(input) { const direct = asmrText(asmrPick(input, ['url', 'path', 'playUrl', 'videoUrl'], '')); if (asmrPlayable(direct)) return asmrPlaybackResult(direct, direct); - let playerUrl = asmrVersionPlayerURL(input); - if (!playerUrl) { - const detail = await getDetail(input); - const version = detail.resourceGroups[0] && detail.resourceGroups[0].versions[0]; - playerUrl = version ? asmrVersionPlayerURL(version.action) : ''; - } - if (!playerUrl) throw new Error('没有找到 ASMRLIB 播放线路'); + const playerUrl = asmrVersionPlayerURL(input); + const itemId = asmrId(asmrPick(input, ['itemId', 'id'], '')) || + asmrId(asmrPick(input, ['versionId'], '')) || asmrId(playerUrl); + if (!itemId && !asmrUPCode(playerUrl)) throw new Error('缺少 ASMRLIB 内容 ID;请重新打开影片详情'); if (typeof Widget === 'undefined' || !Widget.browser || typeof Widget.browser.fetch !== 'function') { throw new Error('当前 Dreamby 版本不支持设备内播放器解析'); } - const candidates = [playerUrl]; - const itemId = asmrId(asmrPick(input, ['itemId', 'id'], '')); - let media = ''; - let resolvedReferer = playerUrl; - for (let i = 0; i < 2 && !media; i += 1) { - if (!candidates[i] && i === 1 && itemId) { - try { - const detail = await getDetail({ itemId: asmrPayload(itemId) }); - const versions = detail.resourceGroups[0] ? detail.resourceGroups[0].versions : []; - versions.forEach(function (version) { - const alternate = asmrVersionPlayerURL(version.action); - if (alternate && candidates.indexOf(alternate) < 0) candidates.push(alternate); - }); - } catch (_) {} - } - const candidate = candidates[i]; - if (!candidate) continue; - if (asmrUPCode(candidate)) { - try { - media = await asmrResolveUP(candidate); - if (media) { - resolvedReferer = candidate; - break; - } - } catch (_) {} - continue; - } + if (asmrUPCode(playerUrl)) { try { - const result = await Widget.browser.fetch(candidate, { - visible: false, - timeout: 18, - timeoutSeconds: 18, - waitAfterLoad: 1.2, - waitForAny: true, - waitForMediaSource: true, - captureRequests: true, - captureMedia: true, - headers: asmrHeaders(ASMRLIB_BASE + '/') - }); - media = asmrBrowserMedia(result); - if (media) resolvedReferer = candidate; - } catch (_) {} + const media = await asmrBounded('up-api', 5, function () { return asmrResolveUP(playerUrl); }); + if (media) return asmrPlaybackResult(media, playerUrl); + } catch (error) { + if (!itemId) throw new Error('旧 UP 线路解析失败;stage=up-api;请重新打开影片详情选择当前线路'); + } } - if (!media) throw new Error('快速线路和备用线路均未捕获到媒体,请稍后重试'); - return asmrPlaybackResult(media, resolvedReferer); + const visibleValue = asmrPick(input, ['browserVisible'], true); + const visible = !/^(?:false|0|no|off)$/i.test(String(visibleValue)); + const pageURL = ASMRLIB_BASE + '/posts/' + itemId; + // BI needs a human action; AB redirects when top.location == self.location. + // Keep the original page and iframe instead of loading either player alone. + const result = await asmrBounded('page-media', 20, function () { + return Widget.browser.fetch(pageURL, { + visible: visible, timeout: 20, timeoutSeconds: 20, waitAfterLoad: 1.2, + waitForMediaSource: true, headers: asmrHeaders(ASMRLIB_BASE + '/') + }); + }); + const media = asmrCapturedMedia(result); + if (!media.url) { + const html = asmrResponseText(result); + const blobOnly = /blob:/i.test(JSON.stringify(result || {})); + const verification = /验证你是真人|verify.{0,20}human|captcha|turnstile/i.test(html); + const stage = verification || !visible ? 'verification-required' : 'page-media'; + throw new Error('ASMRLIB 未返回可交给播放器的媒体;stage=' + stage + + ';keys=' + Object.keys(result || {}).slice(0, 12).join(',') + ';blobOnly=' + blobOnly + + (stage === 'verification-required' ? ';请启用显示播放验证页面,并手动完成验证或点击播放' : ';若页面内已播放,宿主可能没有回传 iframe 媒体地址')); + } + // A captured media request supplies its own Referer/Origin. The outer ASMR + // page's origin is not necessarily the embedded player's media origin. + return asmrPlaybackResult(media.url, '', Object.assign({ 'User-Agent': ASMRLIB_UA }, media.headers)); +} + +async function asmrBounded(stage, seconds, operation) { + let timer; + try { + return await Promise.race([ + Promise.resolve().then(operation), + new Promise(function (_, reject) { timer = setTimeout(function () { reject(new Error('ASMRLIB 请求超时;stage=' + stage)); }, seconds * 1000); }) + ]); + } catch (error) { + if (String(error && error.message).indexOf('ASMRLIB 请求超时;stage=' + stage) === 0) throw error; + throw new Error('ASMRLIB 请求失败;stage=' + stage + ';请检查网络或宿主浏览器支持'); + } finally { clearTimeout(timer); } } -function asmrPlaybackResult(url, referer) { +function asmrPlaybackResult(url, referer, capturedHeaders) { const container = /\.m3u8(?:[?#]|$)/i.test(url) ? 'm3u8' : (/\.mpd(?:[?#]|$)/i.test(url) ? 'mpd' : 'mp4'); const originMatch = asmrText(referer).match(/^(https?:\/\/[^/]+)/i); return { url: url, container: container, - headers: { Referer: referer || ASMRLIB_BASE + '/', Origin: originMatch ? originMatch[1] : ASMRLIB_BASE, 'User-Agent': ASMRLIB_UA }, + headers: capturedHeaders || { Referer: referer || ASMRLIB_BASE + '/', Origin: originMatch ? originMatch[1] : ASMRLIB_BASE, 'User-Agent': ASMRLIB_UA }, startPositionSeconds: 0, isLive: false, streamKind: container === 'm3u8' ? 'hls' : 'file' }; } diff --git a/tests/adapters.test.cjs b/tests/adapters.test.cjs index eba6a24..d128b60 100644 --- a/tests/adapters.test.cjs +++ b/tests/adapters.test.cjs @@ -175,6 +175,74 @@ test('ASMRLIB category previews come from their own category and load after home const section = await c.getHomeSection({ sectionId: 'categories' }); assert.ok(requested.length >= 3); assert.ok(section.items.every(x => x.previewItems[0].id.startsWith(x.action.pageId + '-'))); }); +const asmrPost = 'becd2651e7d56ca656d27766d036dcee'; +const asmrCurrentPage = '
点击播放按钮以验证你是真人
' }; } }; + await assert.rejects(c.resolvePlayback({ itemId: asmrPost, params: { browserVisible: false } }), /stage=verification-required/); + assert.equal(visible, false); assert.equal(c.getManifest().parameters[0].defaultValue, true); +}); +test('ASMRLIB bounds a hung host browser and preserves the stage without exposing native exceptions', async () => { + const c = load('asmrlib-mini-library.js', { setTimeout: (fn, ms) => setTimeout(fn, ms / 100) }); let calls = 0; + c.Widget.browser = { async fetch() { calls++; return new Promise(() => {}); } }; + await assert.rejects(c.resolvePlayback({ itemId: asmrPost }), /stage=page-media/); assert.equal(calls, 1); + c.Widget.browser.fetch = async () => { throw new Error('Cookie=session-secret https://cdn.test/?token=secret'); }; + await assert.rejects(c.resolvePlayback({ itemId: asmrPost }), error => /stage=page-media/.test(error.message) && !/secret/.test(error.message)); +}); +test('ASMRLIB old UP failure falls back once to the embedded page instead of skipping browser capture', async () => { + const c = load('asmrlib-mini-library.js', { $crypto: { aesDecrypt() { throw new Error('unsupported AES'); } } }); let browser = 0, timeout; + c.Widget.http.get = async (url, options) => { timeout = options.timeout; return { data: 'abcdef123456' }; }; + c.Widget.browser = { async fetch(url) { browser++; assert.equal(url, 'https://asmrlib.com/posts/' + asmrPost); return { mediaURL: 'https://cdn.test/current.mp4' }; } }; + const versionId = 'asmrlib-line://UP/' + encodeURIComponent('https://v.upn.one/#legacycode'); + const result = await c.resolvePlayback({ itemId: asmrPost, versionId }); + assert.equal(result.url, 'https://cdn.test/current.mp4'); assert.equal(browser, 1); assert.equal(timeout, 5); + assert.equal(result.headers.Origin, undefined); assert.equal(result.headers.Referer, undefined); +}); +test('ASMRLIB retains direct media compatibility without requiring a browser', async () => { + const c = load('asmrlib-mini-library.js'); + assert.equal((await c.resolvePlayback({ url: 'https://cdn.test/legacy.mp4' })).url, 'https://cdn.test/legacy.mp4'); +}); test('Jable Forward adds sorting and page parameters once', async () => { const c = load('jable.js'); let captured; c.Widget.http.get = async url => { captured = new URL(url); throw new Error('fixture stop'); };