]*class=["'][^"']*streamit-video-grid-block\b[^"']*["'][^>]*>/gi;
- let start = -1; let match;
- while ((match = blockMarker.exec(html)) && match.index < heading.index) start = match.index;
- if (start < 0) start = heading.index;
- blockMarker.lastIndex = heading.index + heading[0].length;
- const next = blockMarker.exec(html);
- return parseCards(html.slice(start, next ? next.index : html.length), ctx);
-}
-
function listURL(ctx, section, page) { return entryURL(ctx) + '/' + String(section.path || 'videos/recent').replace(/^\/+/, '') + (page > 1 ? '/page/' + page : ''); }
-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 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 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' }; }
-function categoryPreviewCard(s, items) {
- const previews = (items || []).filter(function (item) { return item && (item.poster || item.backdrop); }).slice(0, 3);
- if (!previews.length) return null;
- const lead = previews[0];
- return {
- id: s.id, title: s.title, type: 'category', subtitle: '浏览' + s.title,
- poster: lead.poster || lead.backdrop, backdrop: lead.backdrop || lead.poster,
- imageHeaders: lead.imageHeaders, posterHeaders: lead.posterHeaders || lead.imageHeaders,
- backdropHeaders: lead.backdropHeaders || lead.imageHeaders,
- previewItems: previews, action: categoryAction(s)
- };
-}
+function categoryCard(s) { return { id: s.id, title: s.title, type: 'category', subtitle: '浏览' + s.title, action: categoryAction(s) }; }
function sectionShell(s) { return { id: s.id, title: s.title, style: s.style, lazy: true, moreAction: categoryAction(s), items: [] }; }
function findSection(id) { id = String(id || ''); return SECTIONS.find(function (x) { return x.id === id || x.path === id; }); }
function ranked(items) { return items.map(function (x, i) { const y = Object.assign({}, x); y.rank = i + 1; return y; }); }
@@ -318,90 +293,26 @@ 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, 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 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 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 客户端');
}
-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 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 : {}; }
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) {
- 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 resolveURL(value, base) { try { return new URL(value, base).toString(); } catch (_) { if (/^https?:\/\//i.test(value)) return value; return ''; } }
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 ''; }
function decodeHTML(s) { return String(s || '').replace(/&/g, '&').replace(/"/g, '"').replace(/'|'/g, "'").replace(/</g, '<').replace(/>/g, '>').replace(/(\d+);/g, function (_, n) { return String.fromCharCode(Number(n)); }); }
function cleanText(s) { return decodeHTML(String(s || '').replace(/<[^>]+>/g, ' ')).replace(/\s+/g, ' ').trim(); }
function cleanTitle(s) { return cleanText(s).replace(/^\s*麻豆视频\s*[|\-] */i, '').replace(/\s*[|\-] *麻豆视频\s*$/i, '').trim(); }
-function escapeRegExp(s) { return String(s || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); }
function positiveInt(v, fallback) { const n = Number(v); return isFinite(n) && n > 0 ? Math.floor(n) : fallback; }
function unique(a) { return a.filter(function (x, i) { return x && a.indexOf(x) === i; }); }
function uniqueBy(a, key) { const seen = {}; return a.filter(function (x) { const k = key(x); if (seen[k]) return false; seen[k] = true; return true; }); }
diff --git a/missav-mini-library-CloudFlare.js b/missav-mini-library-CloudFlare.js
index 7646e27..4d143b5 100644
--- a/missav-mini-library-CloudFlare.js
+++ b/missav-mini-library-CloudFlare.js
@@ -4,7 +4,6 @@ const MISSAV_DEFAULT_BASE = 'https://missav.ws';
const MISSAV_DEFAULT_ENTRY = '/dm247/cn';
const MISSAV_LOGO = 'https://missav.ws/favicon.ico';
const MISSAV_DETAIL_PAYLOAD_PREFIX = 'missav://detail?';
-const MISSAV_VERIFY_PAYLOAD_PREFIX = 'missav://verify?';
const MISSAV_UA =
'Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1';
@@ -12,8 +11,8 @@ const WidgetMetadata = {
id: 'missav-mini-library',
name: 'MissAV',
title: 'MissAV',
- version: '1.5.11',
- author: 'Alan huang',
+ version: '1.0.0',
+ author: 'baiPlay',
logo: MISSAV_LOGO,
icon: MISSAV_LOGO,
site: MISSAV_DEFAULT_BASE,
@@ -21,72 +20,43 @@ const WidgetMetadata = {
};
const MISSAV_SECTIONS = [
- { id: 'recommended', title: '推荐给你', path: '/', style: 'discover.spotlight', group: 'japanese', personalizedHome: true, randomCategory: true },
- { id: 'nakadashi', title: '中出', path: '/cn/search/%E4%B8%AD%E5%87%BA', style: 'discover.posterCompact', group: 'japanese', eagerPagination: true },
- { id: 'big-breasts', title: '巨乳', path: '/cn/search/%E5%B7%A8%E4%B9%B3', style: 'discover.posterCompact', group: 'japanese', eagerPagination: true },
- { id: 'married-women', title: '人妻', path: '/cn/search/%E4%BA%BA%E5%A6%BB', style: 'discover.posterCompact', group: 'japanese', eagerPagination: true },
- { id: 'chinese-subtitle', title: '中文字幕', path: '/dm278/cn/chinese-subtitle', style: 'discover.posterCompact', group: 'japanese' },
- { id: 'latest', title: '最近更新', path: '/dm539/cn/new', style: 'discover.ranked', group: 'japanese', eagerPagination: true },
- { id: 'new-release', title: '新作上市', path: '/dm635/cn/release', style: 'discover.spotlight', group: 'japanese', eagerPagination: true },
- { id: 'random', title: '随机', aliases: ['好手气'], path: '/random', style: 'discover.posterCompact', group: 'japanese', randomCategory: true },
- { id: 'uncensored-leak', title: '无码流出', aliases: ['无码影片'], path: '/dm817/cn/uncensored-leak', style: 'discover.posterCompact', group: 'uncensored' },
- { id: 'actresses', title: '女优一览', path: '/cn/actresses', style: 'discover.posterCompact', group: 'japanese', categoryIndex: true },
- { id: 'actress-ranking', title: '女优排行', path: '/cn/actresses/ranking', style: 'discover.ranked', group: 'japanese', categoryIndex: true },
- { id: 'genres', title: '类型', path: '/cn/genres', style: 'discover.posterCompact', group: 'japanese', categoryIndex: true },
- { id: 'makers', title: '发行商', path: '/cn/makers', style: 'discover.posterCompact', group: 'japanese', categoryIndex: true },
- { id: 'vr', title: 'VR', path: '/cn/genres/VR', style: 'discover.posterCompact', group: 'japanese' },
- { id: 'today-hot', title: '今日热门', path: '/dm301/cn/today-hot', style: 'discover.ranked', group: 'japanese', eagerPagination: true },
- { id: 'weekly-hot', title: '本周热门', aliases: ['本週热门'], path: '/dm170/cn/weekly-hot', style: 'discover.ranked', group: 'japanese', eagerPagination: true },
- { id: 'monthly-hot', title: '本月热门', path: '/dm273/cn/monthly-hot', style: 'discover.ranked', group: 'japanese', eagerPagination: true },
-
- { id: 'siro', title: 'SIRO', path: '/dm36/cn/siro', style: 'discover.posterCompact', group: 'amateur' },
- { id: 'luxu', title: 'LUXU', path: '/dm34/cn/luxu', style: 'discover.posterCompact', group: 'amateur' },
- { id: 'gana', title: 'GANA', path: '/dm34/cn/gana', style: 'discover.posterCompact', group: 'amateur' },
- { id: 'prestige-premium', title: 'PRESTIGE PREMIUM', aliases: ['MAAN'], path: '/dm1004/cn/maan', style: 'discover.posterCompact', group: 'amateur' },
- { id: 's-cute', title: 'S-CUTE', aliases: ['SCUTE'], path: '/dm38/cn/scute', style: 'discover.posterCompact', group: 'amateur' },
- { id: 'ara', title: 'ARA', path: '/dm34/cn/ara', style: 'discover.posterCompact', group: 'amateur' },
-
- { id: 'fc2', title: 'FC2', path: '/dm597/cn/fc2', style: 'discover.posterCompact', group: 'uncensored' },
- { id: 'heyzo', title: 'HEYZO', path: '/dm2208642/cn/heyzo', style: 'discover.posterCompact', group: 'uncensored' },
- { id: 'tokyo-hot', title: '东京热', aliases: ['TOKYOHOT'], path: '/dm42/cn/tokyohot', style: 'discover.posterCompact', group: 'uncensored' },
- { id: '1pondo', title: '一本道', path: '/dm5199603/cn/1pondo', style: 'discover.posterCompact', group: 'uncensored', artworkCodePrefixes: ['pondo-'], detailSlugPattern: '^pondo-\\d{6}_\\d{3}$' },
- { id: 'caribbeancom', title: 'Caribbeancom', path: '/dm7704788/cn/caribbeancom', style: 'discover.posterCompact', group: 'uncensored' },
- { id: 'caribbeancompr', title: 'Caribbeancompr', path: '/dm91887/cn/caribbeancompr', style: 'discover.posterCompact', group: 'uncensored', detailSlugPattern: '^\\d{6}_\\d{3}$' },
- { id: '10musume', title: '10musume', path: '/dm7208981/cn/10musume', style: 'discover.posterCompact', group: 'uncensored', artworkCodePrefixes: ['musume-'], detailSlugPattern: '^musume-\\d{6}_\\d{2}$' },
- { id: 'pacopacomama', title: 'pacopacomama', path: '/dm3600557/cn/pacopacomama', style: 'discover.posterCompact', group: 'uncensored', artworkCodePrefixes: ['pacopacomama-'], detailSlugPattern: '^pacopacomama-\\d{6}_\\d{3}$' },
- { id: 'gachinco', title: 'Gachinco', path: '/dm150/cn/gachinco', style: 'discover.posterCompact', group: 'uncensored', artworkCodePrefixes: ['gachi'] },
- { id: 'xxx-av', title: 'XXX-AV', aliases: ['XXXAV'], path: '/dm42/cn/xxxav', style: 'discover.posterCompact', group: 'uncensored' },
- { id: 'marriedslash', title: '人妻斩', path: '/dm37/cn/marriedslash', style: 'discover.posterCompact', group: 'uncensored', artworkCodePrefixes: ['c0930-'] },
- { id: 'naughty4610', title: '顽皮 4610', path: '/dm33/cn/naughty4610', style: 'discover.posterCompact', group: 'uncensored', artworkCodePrefixes: ['h4610-'] },
- { id: 'naughty0930', title: '顽皮 0930', path: '/dm37/cn/naughty0930', style: 'discover.posterCompact', group: 'uncensored', artworkCodePrefixes: ['h0930-'] },
-
- { id: 'madou', title: '麻豆传媒', path: '/dm63/cn/madou', style: 'discover.posterCompact', group: 'asian' },
- { id: 'twav', title: 'TWAV', path: '/dm31/cn/twav', style: 'discover.posterCompact', group: 'asian' },
- { id: 'furuke', title: 'Furuke', path: '/dm15/cn/furuke', style: 'discover.posterCompact', group: 'asian' },
- { id: 'klive', title: '韩国直播', path: '/cn/klive', style: 'discover.posterCompact', group: 'asian' },
- { id: 'clive', title: '中国直播', path: '/cn/clive', style: 'discover.posterCompact', group: 'asian' }
+ { id: 'new-release', title: '新作上市', path: '/new', style: 'discover.spotlight' },
+ { id: 'latest', title: '最近更新', path: '/new?sort=published_at', style: 'discover.ranked' },
+ { id: 'chinese-subtitle', title: '中文字幕', path: '/chinese-subtitle', style: 'discover.posterCompact' },
+ { id: 'uncensored-leak', title: '无码流出', aliases: ['无码影片'], path: '/uncensored-leak', style: 'discover.posterCompact' },
+ { id: 'today-hot', title: '今日热门', path: '/today-hot', style: 'discover.ranked' },
+ { id: 'weekly-hot', title: '本周热门', path: '/weekly-hot', style: 'discover.ranked' },
+ { id: 'monthly-hot', title: '本月热门', path: '/monthly-hot', style: 'discover.ranked' },
+ { id: 'fc2', title: 'FC2', path: '/fc2', style: 'discover.posterCompact' },
+ { id: 'heyzo', title: 'HEYZO', path: '/heyzo', style: 'discover.posterCompact' },
+ { id: 'tokyo-hot', title: '东京热', path: '/tokyo-hot', style: 'discover.posterCompact' },
+ { id: '1pondo', title: '一本道', path: '/1pondo', style: 'discover.posterCompact' },
+ { id: 'caribbeancom', title: 'Caribbeancom', path: '/caribbeancom', style: 'discover.posterCompact' },
+ { id: 'caribbeancompr', title: 'Caribbeancompr', path: '/caribbeancompr', style: 'discover.posterCompact' },
+ { id: '10musume', title: '10musume', path: '/10musume', style: 'discover.posterCompact' },
+ { id: 'pacopacomama', title: 'pacopacomama', path: '/pacopacomama', style: 'discover.posterCompact' },
+ { id: 'gachinco', title: 'Gachinco', path: '/gachinco', style: 'discover.posterCompact' },
+ { id: 'xxx-av', title: 'XXX-AV', path: '/xxx-av', style: 'discover.posterCompact' },
+ { id: 'siro', title: 'SIRO', path: '/siro', style: 'discover.posterCompact' },
+ { id: 'luxu', title: 'LUXU', path: '/luxu', style: 'discover.posterCompact' },
+ { id: 'gana', title: 'GANA', path: '/gana', style: 'discover.posterCompact' },
+ { id: 'prestige-premium', title: 'PRESTIGE PREMIUM', path: '/prestige-premium', style: 'discover.posterCompact' },
+ { id: 's-cute', title: 'S-CUTE', path: '/s-cute', style: 'discover.posterCompact' },
+ { id: 'ara', title: 'ARA', path: '/ara', style: 'discover.posterCompact' },
+ { id: 'madou', title: '麻豆传媒', path: '/madou', style: 'discover.posterCompact' },
+ { id: 'twav', title: 'TWAV', path: '/twav', style: 'discover.posterCompact' },
+ { id: 'vr', title: 'VR', path: '/vr', style: 'discover.posterCompact' }
];
-const MISSAV_CATEGORY_GROUPS = [
- { id: 'japanese', title: '日本 AV / 综合分类' },
- { id: 'amateur', title: '素人' },
- { id: 'uncensored', title: '无码影片' },
- { id: 'asian', title: '亚洲 AV' }
-];
-
-const MISSAV_HOME_MEDIA_SECTION_IDS = [
- 'recommended',
- 'nakadashi',
- 'big-breasts',
- 'married-women',
- 'new-release',
- 'latest',
- 'chinese-subtitle',
- 'uncensored-leak',
- 'random',
- 'today-hot',
- 'weekly-hot',
- 'monthly-hot'
+const MISSAV_PRIMARY_CATEGORIES = [
+ { id: 'latest', title: '最近更新', path: '/new?sort=published_at', subtitle: '按发布时间更新' },
+ { id: 'new-release', title: '新作上市', path: '/new', subtitle: '最新上架作品' },
+ { id: 'uncensored-leak', title: '无码流出', path: '/uncensored-leak', subtitle: '无码流出专区' },
+ { id: 'actresses', title: '女优一览', path: '/actresses', subtitle: '按女优浏览' },
+ { id: 'actress-ranking', title: '女优排行', path: '/actresses/ranking', subtitle: '站内女优排行' },
+ { id: 'genres', title: '类型', path: '/genres', subtitle: '按题材类型浏览' },
+ { id: 'makers', title: '发行商', path: '/makers', subtitle: '按发行商浏览' },
+ { id: 'vr', title: 'VR', path: '/genres/VR', subtitle: 'VR 作品专区' }
];
function getManifest() {
@@ -144,82 +114,19 @@ function getManifest() {
defaultValue: MISSAV_DEFAULT_ENTRY,
required: true,
description: '例如 /dm247/cn。MissAV 的 dm 前缀会变化,失效时可替换为当前可访问入口。'
- },
- {
- name: 'backupBaseURLs',
- title: '备用站点地址',
- type: 'input',
- defaultValue: 'https://missav.ai,https://missav123.com',
- required: false,
- description: '多个域名用逗号或换行分隔。网络不稳或主域名被拦截时会自动尝试备用域名。'
- },
- {
- name: 'enableBrowserFallback',
- title: 'Cloudflare 浏览器兜底',
- type: 'boolean',
- defaultValue: true,
- required: false,
- description: '遇到 Cloudflare 或普通 HTTP 失败时,使用 App 浏览器请求一次并复用浏览器 Cookie。'
- },
- {
- name: 'browserVisible',
- title: '旧版显示窗口开关',
- type: 'boolean',
- defaultValue: false,
- required: false,
- description: '为兼容旧设置保留。自动请求始终隐藏;需要验证时请点击“手动完成验证”卡片。'
- },
- {
- name: 'automaticBrowserFallback',
- title: '自动静默浏览器兜底',
- type: 'boolean',
- defaultValue: true,
- required: false,
- description: '普通 HTTP 命中 Cloudflare 时静默尝试一次;关闭后只显示手动验证卡片。'
- },
- {
- name: 'verificationCooldownMinutes',
- title: '验证重试间隔分钟',
- type: 'number',
- defaultValue: 10,
- required: false,
- description: '静默验证失败后暂停自动重试,避免浏览时连续触发 Cloudflare。'
- },
- {
- name: 'requestTimeoutSeconds',
- title: '请求超时秒数',
- type: 'number',
- defaultValue: 45,
- required: false,
- description: '网络差时可调大到 60-90 秒。'
- },
- {
- name: 'cacheMinutes',
- title: '页面缓存分钟',
- type: 'number',
- defaultValue: 20,
- required: false,
- description: '短期缓存首页、分类、详情 HTML,减少重复触发 Cloudflare。'
- },
- {
- name: 'categoryRestoreMinutes',
- title: '分类浏览进度保留分钟',
- type: 'number',
- defaultValue: 60,
- required: false,
- description: '从影片详情返回分类时,恢复已经加载的分页项目,避免列表退回并卡在 12 项。'
}
]
};
}
async function getHome(ctx) {
- ctx = normalizeContext(ctx);
- const html = await safeFetch(ctx, entryURL(ctx), baseURL(ctx) + '/');
+ const html = await safeFetch(entryURL(ctx), baseURL(ctx) + '/');
const hero = parseCards(sectionBlock(html, '推荐给你'), '推荐给你', ctx).slice(0, 10).map(toWideItem);
- const mediaSections = homeMediaSections(ctx, parseHomeSections(ctx, html));
- const artwork = categoryArtworkFromSections(ctx, mediaSections);
- const sections = categoryGroupSections(ctx, artwork).concat(mediaSections);
+ const parsedSections = parseHomeSections(ctx, html);
+ const mediaSections = parsedSections.length ? parsedSections : MISSAV_SECTIONS.map(function (section) {
+ return sectionShell(ctx, section);
+ });
+ const sections = [primaryCategoriesSection(ctx)].concat(mediaSections);
return {
pageType: 'home',
@@ -231,81 +138,26 @@ async function getHome(ctx) {
};
}
-function homeMediaSections(ctx, parsedSections) {
- const sections = [];
- const seen = {};
- (parsedSections || []).forEach(function (section) {
- if (!section || !section.id || seen[section.id]) return;
- seen[section.id] = true;
- sections.push(section);
- });
- MISSAV_SECTIONS.filter(function (section) {
- return MISSAV_HOME_MEDIA_SECTION_IDS.indexOf(section.id) >= 0;
- }).forEach(function (section) {
- if (seen[section.id]) return;
- seen[section.id] = true;
- sections.push(sectionShell(ctx, section));
- });
- return sections;
-}
-
-function categoryGroupSections(ctx, artwork) {
- const sections = [];
- MISSAV_CATEGORY_GROUPS.forEach(function (group) {
- const categories = MISSAV_SECTIONS.filter(function (category) {
- return category.group === group.id;
- });
- for (let offset = 0; offset < categories.length; offset += 6) {
- const part = Math.floor(offset / 6);
- const groupSectionId = 'missav-categories-' + group.id + (part ? '-' + (part + 1) : '');
- const groupCategories = categories.slice(offset, offset + 6);
- const hasCompleteArtwork = groupCategories.every(function (category) {
- return categoryPreviewItems(
- artwork && artwork[category.id] && artwork[category.id].length
- ? artwork[category.id]
- : cachedCategoryArtwork(ctx, category.id),
- category
- ).length >= 3;
- });
- sections.push({
- id: groupSectionId,
- title: group.title + (part ? '(续 ' + part + ')' : ''),
- style: 'discover.annualWidePreview',
- lazy: !hasCompleteArtwork,
- loadAction: !hasCompleteArtwork
- ? { type: 'custom', id: groupSectionId, sectionId: groupSectionId, title: group.title }
- : undefined,
- items: hasCompleteArtwork
- ? groupCategories.map(function (category) {
- return categoryCard(ctx, category, artwork && artwork[category.id]);
- })
- : []
- });
- }
- });
- return sections;
+function primaryCategoriesSection(ctx) {
+ return {
+ id: 'missav-primary-categories',
+ title: '分类',
+ style: 'discover.annualCategories',
+ lazy: false,
+ items: MISSAV_PRIMARY_CATEGORIES.map(function (category) {
+ return categoryCard(ctx, category);
+ })
+ };
}
-function categoryCard(ctx, category, livePreviewItems) {
- const cachedPreviewItems = cachedCategoryArtwork(ctx, category.id);
- const previewItems = categoryPreviewItems(
- livePreviewItems && livePreviewItems.length
- ? livePreviewItems
- : cachedPreviewItems,
- category
- );
- const cover = previewItems[0] && firstNonEmpty(
- previewItems[0].backdrop,
- previewItems[0].poster,
- previewItems[0].thumbnailURL
- );
- const item = {
+function categoryCard(ctx, category) {
+ return {
id: 'category-' + category.id,
title: category.title,
- subtitle: category.subtitle || '浏览' + category.title + '相关内容',
+ subtitle: category.subtitle || '',
type: 'category',
aspectRatio: '16:9',
- previewItems: previewItems,
+ previewItems: [],
action: {
type: 'category',
id: category.id,
@@ -315,273 +167,37 @@ function categoryCard(ctx, category, livePreviewItems) {
itemAspectRatio: '16:9'
}
};
- if (cover) {
- item.poster = cover;
- item.backdrop = cover;
- item.thumbnailURL = cover;
- item.posterPath = cover;
- item.backdropPath = cover;
- item.image = cover;
- item.imageURL = cover;
- item.posterURL = cover;
- item.backdropURL = cover;
- item.cover = cover;
- item.coverURL = cover;
- item.imageFit = 'fill';
- item.imageHeaders = imageHeaders(ctx, categoryURL(ctx, category.path));
- item.posterHeaders = item.imageHeaders;
- item.backdropHeaders = item.imageHeaders;
- }
- return item;
-}
-
-function categoryArtworkFromSections(ctx, sections) {
- const artwork = {};
- (sections || []).forEach(function (section) {
- if (!section || !section.id || !section.items || !section.items.length) return;
- const previews = categoryPreviewItems(section.items, findSection(section.id));
- if (!previews.length) return;
- artwork[section.id] = previews;
- rememberCategoryArtwork(ctx, section.id, previews);
- });
- return artwork;
-}
-
-function categoryPreviewItems(items, category) {
- const prefixes = category && Array.isArray(category.artworkCodePrefixes)
- ? category.artworkCodePrefixes.map(function (value) {
- return String(value || '').toLowerCase();
- }).filter(Boolean)
- : [];
- return (items || []).filter(function (item) {
- if (!item || !firstNonEmpty(item.backdrop, item.poster, item.thumbnailURL)) return false;
- if (!prefixes.length && !(category && category.detailSlugPattern)) return true;
- if (!categoryMatchesItem(item, category)) return false;
- if (!prefixes.length) return true;
- const target = stringValue(item.action && (item.action.url || item.action.itemId || item.action.id)).toLowerCase();
- const title = stringValue(item.title).toLowerCase();
- return prefixes.some(function (prefix) {
- return target.indexOf('/' + prefix) >= 0 || title.indexOf(prefix) >= 0;
- });
- }).slice(0, 3).map(function (item, index) {
- const image = firstNonEmpty(item.backdrop, item.poster, item.thumbnailURL);
- return {
- id: item.id,
- title: item.title,
- type: item.type || 'movie',
- rank: index + 1,
- poster: image,
- backdrop: image,
- thumbnailURL: image,
- aspectRatio: '16:9',
- action: item.action
- };
- });
-}
-
-function categoryMatchesItem(item, category) {
- if (!category || !category.detailSlugPattern) return true;
- const target = stringValue(item && item.action && (item.action.url || item.action.itemId || item.action.id));
- const slug = categorySlug(pathOf(target)).toLowerCase();
- try {
- return new RegExp(category.detailSlugPattern, 'i').test(slug);
- } catch (error) {
- return true;
- }
-}
-
-function filterCategoryItems(items, category) {
- if (!category || !category.detailSlugPattern) return items || [];
- return (items || []).filter(function (item) {
- return categoryMatchesItem(item, category);
- });
}
async function getHomeSection(ctx) {
- ctx = normalizeContext(ctx);
const sectionId = stringValue(ctx && (ctx.sectionId || ctx.id));
- const categoryGroup = categoryGroupFromSectionId(sectionId);
- if (categoryGroup) return loadCategoryGroupSection(ctx, categoryGroup);
const section = findSection(sectionId) || MISSAV_SECTIONS[0];
- if (section.personalizedHome) return personalizedRecommendationSection(ctx, section);
- const url = categoryURL(ctx, section.path);
try {
- const html = await fetchText(ctx, url, entryURL(ctx));
- const items = filterCategoryItems(
- parseCards(mediaListBlock(html), section.title, ctx),
- section
- ).slice(0, 18);
- rememberCategoryArtwork(ctx, section.id, items);
+ const html = await fetchText(ctx, categoryURL(ctx, section.path));
return {
id: section.id,
title: section.title,
style: section.style,
lazy: false,
moreAction: categoryAction(ctx, section),
- items: items
+ items: parseCards(html, section.title, ctx).slice(0, 18)
};
} catch (error) {
- return verificationSection(ctx, section.id, section.title, section.style, url, error);
+ return emptySection(section.id, section.title, section.style, error);
}
}
-function categoryGroupFromSectionId(sectionId) {
- const value = stringValue(sectionId);
- const match = /^missav-categories-([a-z]+)(?:-(\d+))?$/.exec(value);
- if (!match) return null;
- const group = MISSAV_CATEGORY_GROUPS.find(function (item) {
- return item.id === match[1];
- });
- if (!group) return null;
- const partNumber = Math.max(1, Number(match[2] || 1));
- const categories = MISSAV_SECTIONS.filter(function (category) {
- return category.group === group.id;
- }).slice((partNumber - 1) * 6, partNumber * 6);
- if (!categories.length) return null;
- return {
- id: value,
- title: group.title + (partNumber > 1 ? '(续 ' + (partNumber - 1) + ')' : ''),
- categories: categories
- };
-}
-
-async function loadCategoryGroupSection(ctx, groupSection) {
- const artwork = {};
- const categories = groupSection.categories || [];
- for (let offset = 0; offset < categories.length; offset += 2) {
- const batch = categories.slice(offset, offset + 2);
- const results = await Promise.all(batch.map(function (category) {
- return loadCategoryPreviewItems(ctx, category);
- }));
- batch.forEach(function (category, index) {
- artwork[category.id] = results[index];
- });
- }
- return {
- id: groupSection.id,
- title: groupSection.title,
- style: 'discover.annualWidePreview',
- lazy: false,
- items: categories.map(function (category) {
- return categoryCard(ctx, category, artwork[category.id]);
- })
- };
-}
-
-async function loadCategoryPreviewItems(ctx, category) {
- const cached = categoryPreviewItems(cachedCategoryArtwork(ctx, category.id), category);
- if (cached.length >= 3) return cached;
- try {
- const url = categoryURL(ctx, category.path);
- let html;
- if (category.personalizedHome) {
- html = await fetchText(ctx, entryURL(ctx), entryURL(ctx));
- } else if (category.categoryIndex && isActressIndexURL(url)) {
- html = await fetchActressIndexText(ctx, url, entryURL(ctx));
- } else {
- html = await fetchText(ctx, url, entryURL(ctx));
- }
- let items;
- if (category.personalizedHome) {
- items = parseCards(sectionBlock(html, category.title), category.title, ctx);
- if (!items.length) items = parseCards(mediaListBlock(html), category.title, ctx);
- } else if (category.categoryIndex) {
- items = parseCategoryCards(ctx, html, category.title);
- if (!items.length) items = parseCards(mediaListBlock(html), category.title, ctx);
- } else {
- items = filterCategoryItems(
- parseCards(mediaListBlock(html), category.title, ctx),
- category
- );
- }
- const previews = categoryPreviewItems(items, category);
- if (previews.length) rememberCategoryArtwork(ctx, category.id, previews);
- return previews;
- } catch (error) {
- return cached;
- }
-}
-
-async function personalizedRecommendationSection(ctx, section) {
- let items = [];
- const homeURL = entryURL(ctx);
- const browserText = await browserHTML(ctx, homeURL, homeURL);
- if (browserText) {
- items = parseCards(sectionBlock(browserText, section.title), section.title, ctx).slice(0, 18);
- }
- if (!items.length) {
- try {
- const fallbackHTML = await fetchText(ctx, randomPageURL(ctx, 1), homeURL);
- items = parseCards(fallbackHTML, section.title, ctx).slice(0, 18);
- } catch (error) {
- return verificationSection(ctx, section.id, section.title, section.style, homeURL, error);
- }
- }
- rememberCategoryArtwork(ctx, section.id, items);
- return {
- id: section.id,
- title: section.title,
- style: section.style,
- lazy: false,
- moreAction: categoryAction(ctx, section),
- items: items
- };
-}
-
async function getCategory(ctx) {
- ctx = normalizeContext(ctx);
- const requestedPage = pageFromContext(ctx, 1);
+ const page = positiveInt(contextValue(ctx, 'page'), 1);
const pageId = normalizePageId(ctx, ctx && (ctx.pageId || ctx.id));
- const verificationURL = verificationUrlFromPageId(pageId);
- if (verificationURL) return runVerificationCategory(ctx, pageId, verificationURL);
const section = findSection(pageId);
const primary = findPrimaryCategory(pageId);
const path = section ? section.path : primary ? primary.path : pageId;
- const page = categoryFetchPage(ctx, pageId, requestedPage);
- const url = section && section.randomCategory
- ? randomPageURL(ctx, page)
- : pagedURL(categoryURL(ctx, path), page);
- let html = '';
- try {
- html = isActressWorksURL(url)
- ? await fetchActressCategoryText(ctx, url, entryURL(ctx))
- : isActressIndexURL(url)
- ? await fetchActressIndexText(ctx, url, entryURL(ctx))
- : await fetchText(ctx, url, entryURL(ctx));
- } catch (error) {
- return verificationCategory(ctx, pageId, section ? section.title : primary ? primary.title : '需要验证', url, error, page);
- }
+ const url = pagedURL(categoryURL(ctx, path), page);
+ const html = await fetchText(ctx, url);
const title = section ? section.title : primary ? primary.title : pageTitle(html) || 'MissAV';
- const categoryIndex = !!(section && section.categoryIndex);
- const categoryItems = categoryIndex ? parseCategoryCards(ctx, html, title) : [];
- const items = categoryItems.length
- ? []
- : filterCategoryItems(parseCards(mediaListBlock(html), title, ctx), section);
- const fallbackCategoryItems = items.length || categoryItems.length ? [] : parseCategoryCards(ctx, html, title);
- const listItems = items.length
- ? items
- : categoryItems.length
- ? categoryItems
- : fallbackCategoryItems;
- if (section && items.length) rememberCategoryArtwork(ctx, section.id, items);
- const pagination = section && section.randomCategory
- ? { hasMore: true, totalPages: 99 }
- : section && section.id === 'actress-ranking'
- ? { hasMore: false, totalPages: 1 }
- : paginationInfo(html, page, listItems.length);
- if (items.length && requestedPage === 1 && section && section.eagerPagination) {
- await prefetchNextCategoryPage(ctx, pageId, section, page, pagination);
- }
- const restored = listItems.length
- ? rememberCategoryPage(ctx, pageId, page, listItems, pagination)
- : null;
- const responseItems = restored && restored.items.length
- ? restored.items
- : items.length
- ? items
- : listItems;
- const responsePage = restored ? restored.page : page;
- const responseHasMore = restored ? restored.hasMore : items.length ? pagination.hasMore : false;
+ const items = parseCards(html, title, ctx);
+ const categoryItems = items.length ? [] : parseCategoryCards(ctx, html, title);
return {
pageType: 'category',
@@ -589,28 +205,17 @@ async function getCategory(ctx) {
title: title,
style: items.length ? 'media.posterGrid' : 'discover.annualCategories',
itemAspectRatio: '16:9',
- items: responseItems,
- page: responsePage,
- currentPage: responsePage,
- pageIndex: responsePage,
- nextPage: responseHasMore ? responsePage + 1 : undefined,
- totalPages: restored && restored.totalPages ? restored.totalPages : pagination.totalPages,
- pagecount: restored && restored.totalPages ? restored.totalPages : pagination.totalPages,
- hasMore: responseHasMore
+ items: items.length ? items : categoryItems,
+ page: page,
+ hasMore: items.length ? hasNextPage(html, page) : false
};
}
async function getDetail(ctx) {
- ctx = normalizeContext(ctx);
const detailURL = detailUrlFromContext(ctx);
if (!detailURL) throw new Error('MissAV 详情参数无效');
- let html = '';
- try {
- html = await fetchText(ctx, detailURL, entryURL(ctx));
- } catch (error) {
- return verificationDetail(ctx, detailURL, error);
- }
+ const html = await fetchText(ctx, detailURL);
const fallbackTitle = titleFromUrl(detailURL);
const title = cleanText(
firstNonEmpty(
@@ -642,21 +247,9 @@ async function getDetail(ctx) {
firstMatch(html, /(\d{1,2}:\d{2}:\d{2})/i)
);
const releaseDate = firstMatch(html, /(\d{4}-\d{2}-\d{2})/);
- let detailPlayableURL = absoluteURL(ctx, extractPlayableURL(html));
- if (!detailPlayableURL) detailPlayableURL = await extractPlayableFromLinkedPlayers(ctx, html, detailURL);
- let resourceGroups = await playbackGroups(detailURL, title, detailPlayableURL, ctx, html);
- if (resourceVersionCount(resourceGroups) <= 1) {
- const browserPlayableURL = await extractFromBrowser(detailURL, detailURL);
- if (browserPlayableURL && browserPlayableURL !== detailPlayableURL) {
- const browserGroups = await playbackGroups(detailURL, title, browserPlayableURL, ctx, html);
- if (resourceVersionCount(browserGroups) > resourceVersionCount(resourceGroups)) {
- detailPlayableURL = browserPlayableURL;
- resourceGroups = browserGroups;
- }
- }
- }
- const versions = resourceGroups[0] && resourceGroups[0].versions || [];
- const defaultVersion = versions[0];
+ const detailPlayableURL = absoluteURL(ctx, extractPlayableURL(html));
+ const resourceGroups = playbackGroups(detailURL, title, detailPlayableURL, ctx);
+ const defaultVersion = resourceGroups[0] && resourceGroups[0].versions && resourceGroups[0].versions[0];
return {
id: makeItemId(detailURL, title, poster),
@@ -678,19 +271,19 @@ async function getDetail(ctx) {
remarks: durationText || code,
resourceGroups: resourceGroups,
resourceSummary: {
- versionCount: versions.length,
+ versionCount: defaultVersion ? 1 : 0,
episodeCount: 0,
defaultVersionId: defaultVersion ? defaultVersion.id : ''
},
- mediaSources: defaultVersion && defaultVersion.url ? [
+ mediaSources: detailPlayableURL ? [
{
- id: defaultVersion.id,
- name: defaultVersion.name,
- displayName: defaultVersion.name,
+ id: encodePayload({ url: detailURL, title: title, playUrl: detailPlayableURL }),
+ name: 'MissAV HLS',
+ displayName: 'MissAV HLS',
protocol: 'hls',
container: 'm3u8',
- url: defaultVersion.url,
- path: defaultVersion.url,
+ url: detailPlayableURL,
+ path: detailPlayableURL,
headers: playbackHeaders(ctx, detailURL),
header: playbackHeaders(ctx, detailURL),
Header: playbackHeaders(ctx, detailURL),
@@ -715,70 +308,34 @@ async function getDetail(ctx) {
}
async function getResourceVersions(ctx) {
- ctx = normalizeContext(ctx);
const detailURL = detailUrlFromContext(ctx);
const title = stringValue(ctx && (ctx.title || ctx.name)) || titleFromUrl(detailURL);
const direct = playUrlFromContext(ctx);
- if (direct) {
- let directGroups = await playbackGroups(detailURL, title, direct, ctx);
- if (resourceVersionCount(directGroups) <= 1 && detailURL) {
- const browserPlayableURL = await extractFromBrowser(detailURL, detailURL);
- if (browserPlayableURL && browserPlayableURL !== direct) {
- const browserGroups = await playbackGroups(detailURL, title, browserPlayableURL, ctx);
- if (resourceVersionCount(browserGroups) > resourceVersionCount(directGroups)) directGroups = browserGroups;
- }
- }
- return directGroups;
- }
+ if (direct) return playbackGroups(detailURL, title, direct, ctx);
try {
- const html = await fetchText(ctx, detailURL, entryURL(ctx));
- let playUrl = extractPlayableURL(html);
- if (!playUrl) playUrl = await extractPlayableFromLinkedPlayers(ctx, html, detailURL);
- let groups = await playbackGroups(detailURL, title, playUrl, ctx, html);
- if (resourceVersionCount(groups) <= 1) {
- const browserPlayableURL = await extractFromBrowser(detailURL, detailURL);
- if (browserPlayableURL && browserPlayableURL !== playUrl) {
- const browserGroups = await playbackGroups(detailURL, title, browserPlayableURL, ctx, html);
- if (resourceVersionCount(browserGroups) > resourceVersionCount(groups)) groups = browserGroups;
- }
- }
- return groups;
+ const html = await fetchText(ctx, detailURL);
+ return playbackGroups(detailURL, title, extractPlayableURL(html), ctx);
} catch (error) {
- return [];
+ return playbackGroups(detailURL, title, '', ctx);
}
}
-async function resolvePlaybackWithinBudget(ctx) {
- ctx = normalizeContext(ctx);
- const requestedQuality = qualityFromContext(ctx);
+async function resolvePlayback(ctx) {
const direct = firstNonEmpty(playUrlFromContext(ctx), ctx && ctx.url, ctx && ctx.playUrl, ctx && ctx.videoUrl);
- const stableDetailURL = detailUrlFromContext(ctx);
- if (!isDetailPageURL(stableDetailURL) && !requestedQuality && isPlayableURL(direct) && !isDetailPageURL(direct)) {
+ if (isPlayableURL(direct) && !isDetailPageURL(direct)) {
return playbackResult(ctx, direct, ctx && ctx.referer);
}
const detailURL = detailUrlFromContext(ctx);
if (!detailURL) throw new Error('MissAV 播放参数无效');
- let html = '';
- try {
- html = await fetchText(ctx, detailURL, entryURL(ctx));
- } catch (error) {
- recordPlaybackFailure(ctx, error);
- }
- let masterURL = extractPlayableURL(html);
- if (!masterURL) masterURL = await extractPlayableFromLinkedPlayers(ctx, html, detailURL);
- if (!masterURL) masterURL = await extractFromBrowser(detailURL, detailURL, ctx);
- let url = masterURL;
- if (masterURL) {
- const qualities = await discoverAvailableQualities(ctx, masterURL, detailURL, html);
- // A valid master can let the native player choose a variant. Optional quality
- // discovery must not spend a browser round or prevent that handoff.
- const selected = selectQuality(qualities, requestedQuality);
- if (selected && selected.url) url = selected.url;
- }
+ const html = await fetchText(ctx, detailURL);
+ const url = firstNonEmpty(
+ extractPlayableURL(html),
+ await extractPlayableFromLinkedPlayers(ctx, html, detailURL),
+ await extractFromBrowser(detailURL, detailURL)
+ );
if (!url) {
- const failures = ctx.__playbackBudget.failures.join(';');
- throw new Error('未能解析到 MissAV 播放地址;stage=media-discovery' + (failures ? ';' + failures : '') + '。请检查站点地址;若提示验证,请先手动完成验证。');
+ throw new Error('未能解析到 MissAV 播放地址。站点可能启用了 Cloudflare 或更换了播放器脚本,请尝试更新入口路径或在 App 侧启用浏览器请求。');
}
return playbackResult(ctx, absoluteURL(ctx, url), detailURL);
}
@@ -829,36 +386,18 @@ async function play(flagOrInput, id) {
}
async function search(ctx) {
- ctx = normalizeContext(ctx);
const query = stringValue(ctx && (ctx.query || ctx.keyword || ctx.text));
if (!query) return { pageType: 'search', title: '搜索结果', items: [] };
- const page = pageFromContext(ctx, 1);
+ const page = positiveInt(contextValue(ctx, 'page'), 1);
const url = pagedURL(categoryURL(ctx, '/search/' + encodeURIComponent(query.replace(/\\/g, ''))), page);
- let html = '';
- try {
- html = await fetchText(ctx, url, entryURL(ctx));
- } catch (error) {
- return {
- pageType: 'search',
- title: '搜索结果',
- items: [verificationCard(ctx, '需要真人验证', url, error)],
- page: page,
- hasMore: false
- };
- }
+ const html = await fetchText(ctx, url);
const items = parseCards(html, query, ctx);
- const pagination = paginationInfo(html, page, items.length);
return {
pageType: 'search',
title: '搜索结果',
items: items,
page: page,
- currentPage: page,
- pageIndex: page,
- nextPage: pagination.hasMore ? page + 1 : undefined,
- totalPages: pagination.totalPages,
- pagecount: pagination.totalPages,
- hasMore: pagination.hasMore
+ hasMore: hasNextPage(html, page)
};
}
@@ -877,39 +416,15 @@ async function matchResources(ctx) {
return { results: (results.items || []).slice(0, 8) };
}
-async function onAction(ctx) {
- ctx = normalizeContext(ctx);
- const nestedAction = ctx && ctx.action && typeof ctx.action === 'object' ? ctx.action : {};
- const name = stringValue(firstNonEmpty(
- ctx && ctx.name,
- nestedAction.name,
- ctx && ctx.actionName,
- nestedAction.id,
- ctx && ctx.id
- ));
- const payload = nestedAction.payload || (ctx && ctx.payload) || nestedAction || ctx || {};
- if (name !== 'verifyCloudflare') return { handled: false };
- const url = stringValue(payload.url || ctx.url) || entryURL(ctx);
- const html = await browserHTML(ctx, url, url, true);
- if (isUsableHTML(html)) setCachedText(ctx, url, html);
- return {
- handled: true,
- ok: isUsableHTML(html),
- message: isUsableHTML(html) ? '验证完成,可以返回刷新当前页面。' : '验证没有完成,请确认页面已加载并通过真人验证。'
- };
-}
-
async function extractPlayableFromLinkedPlayers(ctx, html, referer) {
- const urls = extractPlayerURLs(ctx, html, referer).filter(function (url) {
- return !/googletagmanager\.com|snaptrckr\.|myavlive\.com|mayzaent\.com|rallytrck\./i.test(originOf(url));
- }).slice(0, ctx && ctx.__playbackBudget ? 1 : 5);
+ const urls = extractPlayerURLs(ctx, html, referer);
for (let index = 0; index < urls.length; index += 1) {
try {
- const playerHTML = await fetchText(ctx, urls[index], referer);
+ const playerHTML = await fetchText(ctx, urls[index]);
const playable = extractPlayableURL(playerHTML);
if (playable) return playable;
} catch (error) {
- recordPlaybackFailure(ctx, error);
+ // Ignore broken auxiliary player pages and continue with browser fallback.
}
}
return '';
@@ -941,47 +456,6 @@ function baseURL(ctx) {
) || MISSAV_DEFAULT_BASE).replace(/\/+$/, '');
}
-function normalizeContext(ctx) {
- if (ctx === undefined || ctx === null) return {};
- if (typeof ctx === 'string') {
- const text = ctx.trim();
- if (!text) return {};
- if (text[0] === '{' && text[text.length - 1] === '}') {
- try {
- const parsed = JSON.parse(text);
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
- } catch (error) {
- return { query: text };
- }
- }
- return { query: text };
- }
- return typeof ctx === 'object' ? ctx : {};
-}
-
-function pageFromContext(ctx, fallback) {
- const input = normalizeContext(ctx);
- const pagination = input.pagination && typeof input.pagination === 'object' ? input.pagination : {};
- const pageInfo = input.pageInfo && typeof input.pageInfo === 'object' ? input.pageInfo : {};
- return positiveInt(firstNonEmpty(
- contextValue(input, 'page'),
- contextValue(input, 'pg'),
- contextValue(input, 'currentPage'),
- contextValue(input, 'pageNumber'),
- contextValue(input, 'pageIndex'),
- pagination.page,
- pagination.pg,
- pagination.currentPage,
- pagination.pageNumber,
- pagination.pageIndex,
- pageInfo.page,
- pageInfo.pg,
- pageInfo.currentPage,
- pageInfo.pageNumber,
- pageInfo.pageIndex
- ), fallback);
-}
-
function entryPath(ctx) {
let value = stringValue(
contextValue(ctx, 'entryPath') ||
@@ -1027,187 +501,39 @@ function pagedURL(url, page) {
return url + (url.indexOf('?') >= 0 ? '&' : '?') + 'page=' + page;
}
-function isActressWorksURL(url) {
- const path = pathOf(url).split('?')[0].replace(/\/+$/, '');
- return /\/(?:dm\d+\/)?cn\/actresses\/[^/]+$/i.test(path) &&
- !/\/actresses\/ranking$/i.test(path);
-}
-
-function isActressIndexURL(url) {
- const path = pathOf(url).split('?')[0].replace(/\/+$/, '');
- return /\/(?:dm\d+\/)?cn\/actresses(?:\/ranking)?$/i.test(path);
-}
-
-async function fetchActressCategoryText(ctx, url, referer) {
- const cached = getCachedText(ctx, url);
- if (cached) return cached;
-
- const fastContext = Object.assign({}, ctx || {}, {
- requestTimeoutSeconds: Math.min(numberParam(ctx, 'requestTimeoutSeconds', 45), 12),
- suppressBrowserFailureCooldown: true
+async function fetchText(ctx, url) {
+ const response = await httpGet(url, {
+ headers: requestHeaders(ctx, url),
+ useBrowserCookie: true,
+ attachBrowserCookie: true,
+ useBrowserFallback: true,
+ browserFallback: true,
+ allowBrowserFallback: true
});
-
- let lastError = null;
- try {
- const options = requestOptions(fastContext, referer || url);
- options.useBrowserFallback = false;
- options.browserFallback = false;
- options.allowBrowserFallback = false;
- const response = await httpGet(url, options);
- const text = responseText(response);
- if (isUsableHTML(text, response && response.status, response && response.headers)) {
- setCachedText(ctx, url, text);
- return text;
- }
- throw new Error('HTTP ' + (response && response.status ? response.status : 'empty'));
- } catch (error) {
- lastError = error;
- }
-
- // Actress work pages are challenged more often than ordinary categories.
- // Reuse the verified browser session once with a short timeout instead of
- // immediately appending another verification card to the existing 12 items.
- const browserText = await browserHTML(fastContext, url, referer || url);
- if (isVerifiedTargetHTML(fastContext, url, browserText)) {
- setCachedText(ctx, url, browserText);
- return browserText;
+ const text = responseText(response);
+ if (isCloudflare(text, response && response.status, response && response.headers)) {
+ const browserText = await browserHTML(url, url);
+ if (browserText && !isCloudflare(browserText)) return browserText;
+ throw new Error('MissAV 启用了 Cloudflare 浏览器校验,当前 HTTP 环境无法直接读取页面。');
}
-
- throw new Error(
- '女优作品页读取失败。已短暂尝试复用刚才的验证状态,请点击一次真人验证后重试。' +
- (lastError && lastError.message ? ' 原因:' + lastError.message : '')
- );
+ return text;
}
-async function fetchActressIndexText(ctx, url, referer) {
- const cached = getCachedText(ctx, url);
- if (cached && isVerifiedTargetHTML(ctx, url, cached)) return cached;
-
- const fastContext = Object.assign({}, ctx || {}, {
- requestTimeoutSeconds: Math.min(numberParam(ctx, 'requestTimeoutSeconds', 45), 12),
- suppressBrowserFailureCooldown: true
- });
- let lastError = null;
+async function safeFetch(url, referer) {
try {
- const options = requestOptions(fastContext, referer || url);
- options.useBrowserFallback = false;
- options.browserFallback = false;
- options.allowBrowserFallback = false;
- const response = await httpGet(url, options);
- const text = responseText(response);
- if (isVerifiedTargetHTML(fastContext, url, text)) {
- setCachedText(ctx, url, text);
- return text;
- }
- throw new Error('HTTP ' + (response && response.status ? response.status : 'empty'));
- } catch (error) {
- lastError = error;
- }
-
- const browserText = await browserHTML(fastContext, url, referer || url);
- if (isVerifiedTargetHTML(fastContext, url, browserText)) {
- setCachedText(ctx, url, browserText);
- return browserText;
- }
-
- throw new Error(
- '女优目录读取失败。已短暂尝试复用浏览器验证状态,请手动完成一次真人验证。' +
- (lastError && lastError.message ? ' 原因:' + lastError.message : '')
- );
-}
-
-async function fetchText(ctx, url, referer) {
- if (ctx && ctx.__playbackBudget) {
- // Refresh player authorization on each play and reserve the one browser
- // request for media capture, rather than an HTML-only verification attempt.
- return runPlaybackStage(ctx, 'detail-http', 6, async function () {
- const response = await httpGet(url, requestOptions(ctx, referer || entryURL(ctx)));
- const text = responseText(response);
- const status = responseStatus(response);
- if (!isUsableHTML(text, status, response && response.headers)) {
- throw playbackStageError('detail-http', 'HTTP ' + (status || 'empty') + (isCloudflare(text, status, response && response.headers) ? ';需要验证或站点拒绝请求' : ';页面不可用'));
- }
- return text;
- });
- }
- const cached = getCachedText(ctx, url);
- if (cached) return cached;
-
- const urls = candidateURLs(ctx, url);
- let lastError = null;
- for (let index = 0; index < urls.length; index += 1) {
- const currentURL = urls[index];
- const requestReferer = referer || entryURL(ctx);
- try {
- const response = await httpGet(currentURL, requestOptions(ctx, requestReferer));
- const text = responseText(response);
- if (isUsableHTML(text, responseStatus(response), response && response.headers)) {
- setCachedText(ctx, currentURL, text);
- if (currentURL !== url) setCachedText(ctx, url, text);
- return text;
- }
- if (isCloudflare(text, responseStatus(response), response && response.headers)) {
- const browserText = await browserHTML(ctx, currentURL, requestReferer);
- if (isUsableHTML(browserText)) {
- setCachedText(ctx, currentURL, browserText);
- if (currentURL !== url) setCachedText(ctx, url, browserText);
- return browserText;
- }
- }
- lastError = new Error('HTTP ' + (response && response.status ? response.status : 'empty') + ' ' + currentURL);
- } catch (error) {
- lastError = error;
- const browserText = await browserHTML(ctx, currentURL, requestReferer);
- if (isUsableHTML(browserText)) {
- setCachedText(ctx, currentURL, browserText);
- if (currentURL !== url) setCachedText(ctx, url, browserText);
- return browserText;
- }
- }
- }
- throw new Error('MissAV 页面读取失败,可能是 Cloudflare 验证、网络超时或当前域名不可达。可点击“手动完成验证”,或在“备用站点地址”里填写当前能打开的域名。' + (lastError && lastError.message ? ' 原因:' + lastError.message : ''));
-}
-
-async function safeFetch(ctx, url, referer) {
- try {
- return await fetchText(ctx || { baseURL: originOf(url), entryPath: pathOf(url) }, url, referer);
+ return await fetchText({ baseURL: originOf(url), entryPath: pathOf(url) }, url, referer);
} catch (error) {
return '';
}
}
-function requestOptions(ctx, referer) {
- const timeout = remainingPlaybackSeconds(ctx, ctx && ctx.__playbackBudget ? 6 : numberParam(ctx, 'requestTimeoutSeconds', 45));
- const browserFallback = !(ctx && ctx.__playbackBudget) && boolParam(ctx, 'enableBrowserFallback', true);
- return {
- headers: requestHeaders(ctx, referer),
- timeout: timeout,
- timeoutSeconds: timeout,
- useBrowserCookie: true,
- attachBrowserCookie: true,
- useBrowserFallback: browserFallback,
- browserFallback: browserFallback,
- allowBrowserFallback: browserFallback
- };
-}
-
function httpGet(url, options) {
if (typeof Widget !== 'undefined' && Widget.http && typeof Widget.http.get === 'function') {
return Widget.http.get(url, options || {});
}
- if (typeof Widget !== 'undefined' && Widget.http && typeof Widget.http.request === 'function') {
- const widgetOptions = options || {};
- if (!widgetOptions.method) widgetOptions.method = 'GET';
- return Widget.http.request(url, widgetOptions);
- }
if (typeof $http !== 'undefined' && typeof $http.get === 'function') {
return $http.get(url, options || {});
}
- if (typeof $http !== 'undefined' && typeof $http.request === 'function') {
- const legacyOptions = options || {};
- if (!legacyOptions.method) legacyOptions.method = 'GET';
- return $http.request(url, legacyOptions);
- }
throw new Error('当前运行环境没有可用 HTTP 客户端。');
}
@@ -1239,434 +565,73 @@ function playbackHeaders(ctx, referer) {
function responseText(response) {
if (typeof response === 'string') return response;
if (!response) return '';
- if (typeof response.text === 'string') return response.text;
if (typeof response.data === 'string') return response.data;
if (typeof response.body === 'string') return response.body;
- if (response.data && typeof response.data.html === 'string') return response.data.html;
- if (response.body && typeof response.body.html === 'string') return response.body.html;
- if (typeof response.html === 'string') return response.html;
return String(response.data || response.body || '');
}
-function responseStatus(response) {
- return Number(response && (response.statusCode || response.status)) || 0;
-}
-
-async function browserHTML(ctx, url, referer, forceVisible) {
- if (!boolParam(ctx, 'enableBrowserFallback', true)) return '';
- if (forceVisible !== true && !boolParam(ctx, 'automaticBrowserFallback', true)) return '';
- if (forceVisible !== true && browserRetryBlocked(ctx, url)) return '';
+async function browserHTML(url, referer) {
if (typeof Widget === 'undefined' || !Widget.browser || typeof Widget.browser.fetch !== 'function') return '';
try {
- const visible = forceVisible === true;
- const configuredTimeout = numberParam(ctx, 'requestTimeoutSeconds', 45);
- const timeout = playbackBrowserSeconds(ctx, visible ? Math.min(configuredTimeout, 20) : ctx && ctx.__playbackBudget ? Math.min(configuredTimeout, 12) : configuredTimeout, 1);
- if (!timeout) return '';
const result = await Widget.browser.fetch(url, {
- visible: visible,
- timeout: timeout,
- timeoutSeconds: timeout,
- waitAfterLoad: visible ? 1.5 : 3,
+ visible: false,
+ timeout: 60,
+ waitAfterLoad: 2.5,
+ waitForAny: true,
+ waitForMediaSource: true,
headers: {
'User-Agent': MISSAV_UA,
Referer: referer || url
}
});
- const html = responseText(result.html ? { data: result.html } : result);
- if (isUsableHTML(html)) {
- clearBrowserFailure(url);
- return html;
- }
- rememberBrowserFailure(ctx, url);
- return '';
+ return responseText(result.html ? { data: result.html } : result);
} catch (error) {
- rememberBrowserFailure(ctx, url);
return '';
}
}
-function browserFailureKey(url) {
- const scope = isActressWorksURL(url) || isActressIndexURL(url)
- ? pathOf(url).split('?')[0].replace(/\/+$/, '')
- : '';
- return 'missav:browser-failure:' + originOf(url) + (scope ? ':' + scope : '');
-}
-
-function browserRetryBlocked(ctx, url) {
- const value = cacheGet(browserFailureKey(url));
- return !!(value && Number(value.retryAfter) > Date.now());
-}
-
-function rememberBrowserFailure(ctx, url) {
- if (boolParam(ctx, 'suppressBrowserFailureCooldown', false)) return;
- const minutes = Math.max(0, numberParam(ctx, 'verificationCooldownMinutes', 10));
- if (!minutes) return;
- cacheSet(browserFailureKey(url), { retryAfter: Date.now() + minutes * 60 * 1000 });
-}
-
-function clearBrowserFailure(url) {
- cacheSet(browserFailureKey(url), { retryAfter: 0 });
-}
-
-async function extractFromBrowser(url, referer, ctx) {
- if (ctx && ctx.__playbackBudget && ctx.__playbackBudget.browserCalls >= 1) return '';
- if (!boolParam(ctx, 'enableBrowserFallback', true) || !boolParam(ctx, 'automaticBrowserFallback', true)) {
- recordPlaybackFailure(ctx, new Error('stage=browser-media;浏览器回退已关闭'));
- return '';
- }
- if (typeof Widget === 'undefined' || !Widget.browser || typeof Widget.browser.fetch !== 'function') {
- recordPlaybackFailure(ctx, new Error('stage=browser-media;宿主未提供浏览器接口'));
- return '';
- }
+async function extractFromBrowser(url, referer) {
+ if (typeof Widget === 'undefined' || !Widget.browser || typeof Widget.browser.fetch !== 'function') return '';
try {
- const timeout = playbackBrowserSeconds(ctx, 12, 1);
- const result = await runPlaybackStage(ctx, 'browser-media', timeout, function () { return Widget.browser.fetch(url, {
+ const result = await Widget.browser.fetch(url, {
visible: false,
- timeout: timeout,
- timeoutSeconds: timeout,
+ timeout: 70,
waitAfterLoad: 3,
waitForMediaSource: true,
waitForAny: true,
- captureRequests: true,
- captureMedia: true,
headers: {
'User-Agent': MISSAV_UA,
Referer: referer || url
}
- }); });
- const playable = firstNonEmpty(
+ });
+ return firstNonEmpty(
playableFromBrowserResult(result),
extractPlayableURL(responseText(result)),
extractPlayableURL(result && result.html)
);
- if (!playable) {
- recordPlaybackFailure(ctx, new Error('stage=browser-media;未返回媒体地址;keys=' + Object.keys(result || {}).slice(0, 12).join(',')));
- }
- return playable;
} catch (error) {
- recordPlaybackFailure(ctx, error);
return '';
}
}
-function candidateURLs(ctx, url) {
- const input = stringValue(url);
- const urls = [input];
- const origin = originOf(input);
- const path = input.replace(/^https?:\/\/[^/]+/i, '');
- candidatePathVariants(ctx, path).forEach(function (variant) {
- const next = origin + variant;
- if (urls.indexOf(next) < 0) urls.push(next);
- });
- backupBaseURLs(ctx).forEach(function (base) {
- const root = base.replace(/\/+$/, '');
- if (!root || root === origin) return;
- candidatePathVariants(ctx, path).forEach(function (variant) {
- const next = root + variant;
- if (urls.indexOf(next) < 0) urls.push(next);
- });
- });
- return urls.filter(Boolean);
-}
-
-function candidatePathVariants(ctx, path) {
- const value = path && path[0] === '/' ? path : '/' + stringValue(path);
- const variants = [value];
- if (!isKnownCategoryPath(value)) return variants;
- const match = value.match(/^\/(?:dm\d+\/)?cn(\/[^?#]*)([?#].*)?$/i);
- if (match) {
- const suffix = match[1] || '';
- const query = match[2] || '';
- const cnPath = '/cn' + suffix + query;
- const rootPath = suffix + query;
- if (variants.indexOf(cnPath) < 0) variants.push(cnPath);
- if (variants.indexOf(rootPath) < 0) variants.push(rootPath);
- } else {
- const localePath = localePrefix(ctx) + value;
- const cnPath = '/cn' + value;
- if (variants.indexOf(localePath) < 0) variants.push(localePath);
- if (variants.indexOf(cnPath) < 0) variants.push(cnPath);
- }
- return variants;
-}
-
-function isKnownCategoryPath(path) {
- const clean = String(path || '').split(/[?#]/)[0].replace(/^\/(?:dm\d+\/)?cn/i, '') || '/';
- return MISSAV_SECTIONS.some(function (item) {
- return categorySlug(clean) === categorySlug(item.path);
- });
-}
-
-function backupBaseURLs(ctx) {
- const raw = stringValue(contextValue(ctx, 'backupBaseURLs') || contextValue(ctx, 'backupBaseUrls') || contextValue(ctx, 'backup_base_urls'));
- return raw.split(/[\n,,\s]+/).map(function (value) {
- return value.replace(/\/+$/, '');
- }).filter(function (value) {
- return /^https?:\/\//i.test(value);
- });
-}
-
-function cacheKey(url) {
- return 'missav:html:' + String(url || '');
-}
-
-function categoryArtworkKey(ctx, pageId) {
- return 'missav:category-artwork:v4:' + baseURL(ctx) + ':' + String(pageId || '');
-}
-
-function rememberCategoryArtwork(ctx, pageId, items) {
- const previews = categoryPreviewItems(items, findSection(pageId));
- if (!previews.length) return;
- const key = categoryArtworkKey(ctx, pageId);
- const value = {
- items: previews,
- expiresAt: Date.now() + 7 * 24 * 60 * 60 * 1000
- };
- memoryCache()[key] = value;
- cacheSet(key, value);
-}
-
-function cachedCategoryArtwork(ctx, pageId) {
- const key = categoryArtworkKey(ctx, pageId);
- const now = Date.now();
- let value = memoryCache()[key] || cacheGet(key);
- if (typeof value === 'string') {
- try {
- value = JSON.parse(value);
- } catch (error) {
- value = null;
- }
- }
- if (!value || value.expiresAt <= now || !Array.isArray(value.items)) return [];
- memoryCache()[key] = value;
- return value.items;
-}
-
-function categoryStateKey(ctx, pageId) {
- return 'missav:category-state:v6:' + baseURL(ctx) + ':' + String(pageId || '');
-}
-
-function memoryCache() {
- if (typeof globalThis === 'undefined') return {};
- if (!globalThis.__MISSAV_HTML_CACHE__) globalThis.__MISSAV_HTML_CACHE__ = {};
- return globalThis.__MISSAV_HTML_CACHE__;
-}
-
-function categoryStateFromCache(ctx, pageId) {
- const key = categoryStateKey(ctx, pageId);
- const now = Date.now();
- let state = memoryCache()[key] || cacheGet(key);
- if (typeof state === 'string') {
- try {
- state = JSON.parse(state);
- } catch (error) {
- state = null;
- }
- }
- if (!state || state.expiresAt <= now || !state.pages) return null;
- memoryCache()[key] = state;
- return state;
-}
-
-function randomPageURL(ctx, page) {
- const bucket = ((Math.max(1, Number(page) || 1) - 1) % 99) + 2;
- return baseURL(ctx) + '/random/' + bucket;
-}
-
-async function prefetchNextCategoryPage(ctx, pageId, section, page, pagination) {
- if (!pagination || !pagination.hasMore) return;
- const nextPage = page + 1;
- const state = categoryStateFromCache(ctx, pageId);
- if (state && state.pages && state.pages[String(nextPage)]) return;
- try {
- const url = pagedURL(categoryURL(ctx, section.path), nextPage);
- const html = await fetchText(ctx, url, entryURL(ctx));
- const items = parseCards(html, section.title, ctx);
- if (!items.length) return;
- rememberCategoryPage(ctx, pageId, nextPage, items, paginationInfo(html, nextPage, items.length));
- } catch (error) {
- // Page 1 remains usable; ordinary scrolling can retry the next page later.
- }
-}
-
-function contiguousCategoryPage(state) {
- if (!state || !state.pages || !state.pages['1']) return 0;
- let page = 1;
- while (state.pages[String(page + 1)]) page += 1;
- return page;
-}
-
-function categoryFetchPage(ctx, pageId, requestedPage) {
- if (requestedPage <= 1) return 1;
- const state = categoryStateFromCache(ctx, pageId);
- if (!state || !state.pages[String(requestedPage)]) return requestedPage;
- const lastLoadedPage = contiguousCategoryPage(state);
- return lastLoadedPage >= requestedPage ? lastLoadedPage + 1 : requestedPage;
-}
-
-function rememberCategoryPage(ctx, pageId, page, items, pagination) {
- const key = categoryStateKey(ctx, pageId);
- const ttl = numberParam(ctx, 'categoryRestoreMinutes', 60) * 60 * 1000;
- const existing = categoryStateFromCache(ctx, pageId);
- const state = existing || { pages: {} };
- state.pages[String(page)] = (items || []).slice(0, 120);
- state.hasMoreByPage = state.hasMoreByPage || {};
- state.hasMoreByPage[String(page)] = !!(pagination && pagination.hasMore);
- state.totalPages = Math.max(Number(state.totalPages) || 0, Number(pagination && pagination.totalPages) || 0);
- state.expiresAt = Date.now() + ttl;
-
- const pageNumbers = Object.keys(state.pages).map(Number).filter(function (value) {
- return Number.isFinite(value) && value > 0;
- }).sort(function (a, b) { return a - b; });
- while (pageNumbers.length > 20) {
- const removed = pageNumbers.shift();
- delete state.pages[String(removed)];
- delete state.hasMoreByPage[String(removed)];
- }
- memoryCache()[key] = state;
- cacheSet(key, state);
-
- if (page !== 1) {
- return {
- items: items,
- page: page,
- hasMore: !!(pagination && pagination.hasMore),
- totalPages: Number(pagination && pagination.totalPages) || state.totalPages
- };
- }
-
- const lastContiguousPage = contiguousCategoryPage(state);
- if (lastContiguousPage === 1) {
- return {
- items: items,
- page: 1,
- hasMore: !!(pagination && pagination.hasMore),
- totalPages: Number(pagination && pagination.totalPages) || state.totalPages
- };
- }
-
- const merged = [];
- const seen = {};
- for (let current = 1; current <= lastContiguousPage; current += 1) {
- (state.pages[String(current)] || []).forEach(function (item) {
- const identity = stringValue(item && (item.id || item.url || item.title));
- if (identity && seen[identity]) return;
- if (identity) seen[identity] = true;
- merged.push(item);
- });
- }
- const latestHasMore = state.hasMoreByPage[String(lastContiguousPage)];
- return {
- items: merged.slice(0, 240),
- page: lastContiguousPage,
- hasMore: latestHasMore !== undefined
- ? latestHasMore
- : state.totalPages > lastContiguousPage,
- totalPages: state.totalPages
- };
-}
-
-function getCachedText(ctx, url) {
- const ttl = numberParam(ctx, 'cacheMinutes', 20) * 60 * 1000;
- if (ttl <= 0) return '';
- const key = cacheKey(url);
- const now = Date.now();
- const memory = memoryCache()[key];
- if (memory && memory.expiresAt > now && memory.text) return memory.text;
- const stored = cacheGet(key);
- if (stored && stored.expiresAt > now && stored.text) return stored.text;
- return '';
-}
-
-function setCachedText(ctx, url, text) {
- if (!isUsableHTML(text)) return;
- const configuredMinutes = numberParam(ctx, 'cacheMinutes', 20);
- const minimumMinutes = isActressIndexURL(url) ? 60 : isActressWorksURL(url) ? 30 : 0;
- const ttl = Math.max(configuredMinutes, minimumMinutes) * 60 * 1000;
- if (ttl <= 0) return;
- const value = { text: text, expiresAt: Date.now() + ttl };
- const key = cacheKey(url);
- memoryCache()[key] = value;
- cacheSet(key, value);
-}
-
-function cacheGet(key) {
- try {
- if (typeof Widget !== 'undefined' && Widget.cache && typeof Widget.cache.get === 'function') return Widget.cache.get(key);
- if (typeof $cache !== 'undefined' && typeof $cache.get === 'function') return $cache.get(key);
- } catch (error) {
- return null;
- }
- return null;
-}
-
-function cacheSet(key, value) {
- try {
- if (typeof Widget !== 'undefined' && Widget.cache && typeof Widget.cache.set === 'function') Widget.cache.set(key, value);
- if (typeof $cache !== 'undefined' && typeof $cache.set === 'function') $cache.set(key, value);
- } catch (error) {
- // Cache is opportunistic; ignore unsupported host APIs.
- }
-}
-
-function isUsableHTML(html, status, headers) {
- const text = String(html || '');
- if (!text || text.length < 80) return false;
- if (Number(status) >= 400) return false;
- if (isCloudflare(text, status, headers)) return false;
- return / 0 ? value : fallback;
-}
-
function playableFromBrowserResult(result) {
if (!result) return '';
- const candidates = [];
const keys = ['url', 'mediaURL', 'mediaUrl', 'videoURL', 'videoUrl', 'playURL', 'playUrl', 'src'];
for (let index = 0; index < keys.length; index += 1) {
const value = result[keys[index]];
- if (isPlayableURL(value)) candidates.push(value);
+ if (isPlayableURL(value)) return value;
}
- const arrays = [
- result.mediaSources,
- result.mediaRequests,
- result.capturedRequests,
- result.requests,
- result.responses,
- result.urls
- ];
+ const arrays = [result.mediaSources, result.mediaRequests, result.requests, result.responses, result.urls];
for (let a = 0; a < arrays.length; a += 1) {
const array = arrays[a];
if (!Array.isArray(array)) continue;
for (let i = 0; i < array.length; i += 1) {
const item = array[i];
const value = typeof item === 'string' ? item : firstNonEmpty(item && item.url, item && item.src, item && item.responseURL);
- if (isPlayableURL(value)) candidates.push(value);
+ if (isPlayableURL(value)) return value;
}
}
- return unique(candidates).sort(function (left, right) {
- return playableCandidateScore(right) - playableCandidateScore(left);
- })[0] || '';
-}
-
-function playableCandidateScore(url) {
- const value = String(url || '');
- let score = 0;
- if (/\.m3u8(?:[?#]|$)/i.test(value)) score += 20;
- if (/(?:master|playlist|index)\.m3u8(?:[?#]|$)/i.test(value)) score += 40;
- if (/\/\d{3,4}p(?:\/|[?#]|$)/i.test(value)) score -= 30;
- if (/\.(?:ts|m2ts)(?:[?#]|$)/i.test(value)) score -= 100;
- return score;
+ return '';
}
function parseHomeSections(ctx, html) {
@@ -1719,199 +684,6 @@ function emptySection(id, title, style, error) {
};
}
-function verificationCategory(ctx, id, title, url, error, page) {
- return {
- pageType: 'category',
- id: id,
- title: title || '需要验证',
- style: 'media.posterGrid',
- itemAspectRatio: '16:9',
- items: [verificationCard(ctx, '需要真人验证', url, error)],
- page: page || 1,
- hasMore: false
- };
-}
-
-function verificationSection(ctx, id, title, style, url, error) {
- return {
- id: id,
- title: title,
- style: style || 'discover.posterCompact',
- lazy: false,
- items: [verificationCard(ctx, '需要真人验证', url, error)]
- };
-}
-
-function verificationDetail(ctx, url, error) {
- const title = titleFromUrl(url) || '需要真人验证';
- return {
- pageType: 'detail',
- id: makeItemId(url, title, ''),
- title: '需要真人验证',
- type: 'collection',
- mediaType: 'collection',
- overview: cleanText(error && error.message) || '站点触发了 Cloudflare 真人验证。请返回列表页点击手动验证卡片,完成后刷新。',
- detailImageAspectRatio: '16:9',
- playable: false,
- isPlayable: false,
- resourceGroups: [],
- mediaSources: [],
- playbackSummary: {
- versionCount: 0,
- episodeCount: 0,
- defaultVersionId: ''
- },
- recommendations: [
- {
- id: 'verify',
- title: '访问受限',
- style: 'discover.posterCompact',
- items: [verificationCard(ctx, '手动完成验证', url, error)]
- }
- ]
- };
-}
-
-function verificationCard(ctx, title, url, error) {
- const targetURL = url || entryURL(ctx);
- const pageId = verificationPageId(targetURL);
- return {
- id: pageId,
- title: title || '手动完成验证',
- subtitle: '点击后再打开验证界面,完成后返回刷新',
- overview: cleanText(error && error.message) || '当前网络触发了 Cloudflare 真人验证。',
- type: 'category',
- mediaType: 'category',
- playable: false,
- isPlayable: false,
- aspectRatio: '16:9',
- action: {
- type: 'category',
- id: pageId,
- pageId: pageId,
- title: '手动完成验证',
- url: targetURL,
- itemAspectRatio: '16:9'
- }
- };
-}
-
-async function runVerificationCategory(ctx, pageId, url) {
- const requestedPage = Math.max(pageFromContext(ctx, 1), pageFromURL(url, 1));
- if (requestedPage > 1 || isVerifiedTargetHTML(ctx, url, getCachedText(ctx, url))) {
- return resumeVerifiedCategory(ctx, pageId, url, requestedPage);
- }
- let html = await browserHTML(ctx, url, url, true);
- if (!isVerifiedTargetHTML(ctx, url, html)) {
- const refreshedHTML = await fetchAfterVerification(ctx, url);
- if (isVerifiedTargetHTML(ctx, url, refreshedHTML)) html = refreshedHTML;
- }
- const ok = isVerifiedTargetHTML(ctx, url, html);
- if (ok) {
- setCachedText(ctx, url, html);
- if (isActressWorksURL(url) || isActressIndexURL(url)) {
- resetCategoryState(ctx, normalizePageId(ctx, url));
- return resumeVerifiedCategory(ctx, pageId, url, 1);
- }
- }
- const retryCard = verificationCard(
- ctx,
- '验证未完成,点击重新验证',
- url,
- new Error('浏览器尚未返回可读取的影片页面')
- );
- return {
- pageType: 'category',
- id: pageId,
- title: ok ? '验证完成' : '验证未完成',
- style: 'media.posterGrid',
- itemAspectRatio: '16:9',
- page: 1,
- hasMore: false,
- items: ok
- ? [{
- id: pageId + '-success',
- title: '验证完成',
- subtitle: '请返回原页面继续浏览',
- type: 'collection',
- mediaType: 'collection',
- playable: false,
- isPlayable: false,
- aspectRatio: '16:9',
- action: { type: 'none' }
- }]
- : [retryCard]
- };
-}
-
-async function resumeVerifiedCategory(ctx, verificationPageId, targetURL, page) {
- const stablePageId = normalizePageId(ctx, targetURL);
- const resumedContext = Object.assign({}, ctx || {}, {
- id: stablePageId,
- pageId: stablePageId,
- page: page,
- currentPage: page,
- pageIndex: page
- });
- let result = await getCategory(resumedContext);
- result.id = stablePageId;
- result.verificationTarget = targetURL;
- return result;
-}
-
-function resetCategoryState(ctx, pageId) {
- const key = categoryStateKey(ctx, pageId);
- delete memoryCache()[key];
- cacheSet(key, { pages: {}, expiresAt: 0 });
-}
-
-function isVerifiedTargetHTML(ctx, url, html) {
- if (!isUsableHTML(html)) return false;
- if (isActressWorksURL(url)) {
- return parseCards(html, pageTitle(html) || '女优作品', ctx).length > 0;
- }
- if (isActressIndexURL(url)) {
- return parseCategoryCards(ctx, html, /\/ranking$/i.test(pathOf(url)) ? '女优排行' : '女优一览').length > 0;
- }
- return true;
-}
-
-async function fetchAfterVerification(ctx, url) {
- const retryContext = Object.assign({}, ctx || {}, {
- requestTimeoutSeconds: Math.min(numberParam(ctx, 'requestTimeoutSeconds', 45), 6)
- });
- for (let attempt = 0; attempt < 1; attempt += 1) {
- try {
- await verificationCookieDelay(500);
- const options = requestOptions(retryContext, url);
- options.useBrowserFallback = false;
- options.browserFallback = false;
- options.allowBrowserFallback = false;
- options.useBrowserCookie = true;
- options.attachBrowserCookie = true;
- const response = await httpGet(url, options);
- const html = responseText(response);
- if (isVerifiedTargetHTML(ctx, url, html)) return html;
- } catch (error) {
- // Return a retry card quickly instead of leaving the verification page spinning.
- }
- }
- return '';
-}
-
-function verificationCookieDelay(milliseconds) {
- if (typeof Promise === 'undefined') return null;
- if (typeof setTimeout !== 'function') return Promise.resolve();
- return new Promise(function (resolve) {
- setTimeout(resolve, milliseconds);
- });
-}
-
-function pageFromURL(url, fallback) {
- const match = String(url || '').match(/[?&]page=(\d+)/i);
- return positiveInt(match && match[1], fallback);
-}
-
function categoryAction(ctx, section) {
return {
type: 'category',
@@ -1924,65 +696,32 @@ function categoryAction(ctx, section) {
function parseCategoryCards(ctx, html, fallbackTitle) {
const source = categoryListBlock(html, fallbackTitle);
- const records = {};
- const order = [];
- const pattern = /
]*)>([\s\S]*?)<\/a>/gi;
- let match;
- while ((match = pattern.exec(source))) {
- const href = attr(match[1], 'href');
- if (!isCategoryHref(href)) continue;
+ const items = [];
+ const seen = {};
+ anchorsIn(source).forEach(function (anchor) {
+ const href = anchor.href;
+ const title = cleanText(anchor.text || anchor.title);
+ if (!title || seen[href + title] || isJunkCategoryTitle(title)) return;
+ if (!isCategoryHref(href)) return;
const url = absoluteURL(ctx, href);
- const rawTitle = cleanText(firstNonEmpty(
- attr(match[1], 'title'),
- attr(match[2], 'alt'),
- match[2]
- ));
- const title = categoryCardTitle(rawTitle);
- const image = absoluteURL(ctx, pickImage(match[0]));
- if (!records[url]) {
- records[url] = { url: url, title: '', image: '' };
- order.push(url);
- }
- if (title && !isJunkCategoryTitle(title)) records[url].title = records[url].title || title;
- if (image) records[url].image = records[url].image || image;
- }
-
- return order.map(function (url, index) {
- const record = records[url];
- if (!record || !record.title) return null;
- const item = {
- id: 'category-link-' + index + '-' + encodeURIComponent(record.title),
- title: record.title,
+ seen[href + title] = true;
+ items.push({
+ id: 'category-link-' + items.length + '-' + encodeURIComponent(title),
+ title: title,
subtitle: fallbackTitle || '分类',
type: 'category',
- mediaType: 'category',
- playable: false,
- isPlayable: false,
- aspectRatio: record.image ? '2:3' : '16:9',
+ aspectRatio: '16:9',
action: {
type: 'category',
- id: record.url,
- pageId: record.url,
- title: record.title,
- url: record.url,
+ id: url,
+ pageId: url,
+ title: title,
+ url: url,
itemAspectRatio: '16:9'
}
- };
- if (record.image) {
- item.poster = record.image;
- item.thumbnailURL = record.image;
- item.imageHeaders = imageHeaders(ctx, record.url);
- }
- return item;
- }).filter(Boolean).slice(0, 120);
-}
-
-function categoryCardTitle(value) {
- return cleanText(value)
- .replace(/\s+\d+\s*(?:条|部)影片[\s\S]*$/i, '')
- .replace(/\s+\d{4}\s*出道[\s\S]*$/i, '')
- .replace(/\s+第\s*\d+\s*名[\s\S]*$/i, '')
- .trim();
+ });
+ });
+ return items.slice(0, 80);
}
function categoryListBlock(html, title) {
@@ -1990,8 +729,9 @@ function categoryListBlock(html, title) {
const heading = cleanText(title || '');
const index = heading ? source.search(new RegExp(']*>\\s*' + escapeRegExp(heading), 'i')) : -1;
if (index < 0) return source;
+ const nextScript = source.indexOf('', capturedRequests: [{ url: masterURL, requestHeaders: { Referer: embedURL, Origin: 'https://player.example', 'User-Agent': 'Fixture browser' } }] };
- if (String(url).indexOf(masterURL) === 0) throw new Error('不得用浏览器导航读取 HLS 正文');
+ if (String(url).indexOf(masterURL) === 0) { manifestBrowserRequests++; return { html: masterManifest }; }
throw new Error('unexpected browser URL: ' + url);
}
},
@@ -47,40 +48,47 @@ global.Widget = {
const library = require('./sexbjcam-mini-library.js');
+// Version 1.1.3 returns its original dynamic default line after a failed
+// quality probe; the later cache/deadline recovery implementation is rolled back.
async function run() {
const weakDetail = await library.getDetail({ itemId: detailURL });
- assert.deepStrictEqual(weakDetail.resourceGroups, [], '弱网失败不能被保存成默认线路');
-
- await assert.rejects(
- library.getResourceVersions({ itemId: detailURL, embedURL }),
- (error) => error instanceof Error,
- '弱网资源探测应失败并允许客户端稍后重试'
+ assert.strictEqual(weakDetail.resourceGroups[0].id, 'online', '初版弱网详情保留动态默认线路');
+ assert.deepStrictEqual(
+ weakDetail.resourceGroups[0].versions.map((version) => version.name),
+ ['默认线路']
);
+ assert.strictEqual(weakDetail.resourceGroups[0].versions[0].action.embedURL, embedURL);
+
+ const weakVersions = await library.getResourceVersions({ itemId: detailURL, embedURL });
+ assert.strictEqual(weakVersions.groups[0].id, 'online', '初版弱网资源探测返回原有默认线路');
+ assert.strictEqual(weakVersions.groups[0].versions[0].action.itemId, detailURL);
networkGood = true;
const recovered = await library.getResourceVersions({ itemId: detailURL, embedURL });
assert.deepStrictEqual(
recovered.groups[0].versions.map((version) => version.name),
['1080P', '720P', '480P'],
- '网络恢复后同一视频应重新发现全部画质'
+ '网络恢复后同一视频重新发现全部画质'
);
+ assert.strictEqual(recovered.groups[0].versions[0].default, true);
+ assert(recovered.groups[0].versions.slice(1).every((version) => !version.default));
+ assert(manifestBrowserRequests > 0, '保留初版浏览器读取 HLS 清单的实际流程');
networkGood = false;
- const cached = await library.getResourceVersions({ itemId: detailURL, embedURL });
+ const weakAgain = await library.getResourceVersions({ itemId: detailURL, embedURL });
assert.deepStrictEqual(
- cached.groups[0].versions.map((version) => version.name),
- ['1080P', '720P', '480P'],
- '成功发现后的稳定画质元数据应可跨网络波动复用'
+ weakAgain.groups[0].versions.map((version) => version.name),
+ ['默认线路'],
+ '初版没有持久画质缓存,再次弱网时返回动态默认线路'
);
-
- const serializedCache = JSON.stringify(Array.from(storageValues.values()));
- assert(!serializedCache.includes('token=fresh'), '不得缓存签名播放 URL');
+ assert.strictEqual(storageValues.size, 0, '初版不写入后续版本的画质缓存');
networkGood = true;
const playback = await library.resolvePlayback({ itemId: detailURL, embedURL, versionId: 'quality:720' });
- assert(playback.url.includes('/720/index.m3u8'), '播放总时限内仍应完成播放器与签名清单回退并选择 720P');
- assert.strictEqual(playback.headers.Referer, embedURL, '保持播放器要求的原生请求头');
- console.log('SexBJCam network recovery test passed');
+ assert(playback.url.includes('/720/index.m3u8'), '网络恢复后按原版画质标识选择 720P');
+ assert.strictEqual(playback.headers.Referer, embedURL, '保持原版播放器请求头');
+ assert.strictEqual(playback.headers.Origin, 'https://player.example');
+ console.log('SexBJCam first-upload network behavior test passed');
}
run().catch((error) => {
diff --git a/taolusm-mini-library.js b/taolusm-mini-library.js
index 9b14e1d..0492a2c 100644
--- a/taolusm-mini-library.js
+++ b/taolusm-mini-library.js
@@ -9,8 +9,8 @@ const WidgetMetadata = {
id: 'taolusm-mini-library',
name: '套路SM',
title: '套路SM',
- version: '1.0.2',
- author: 'Alan huang',
+ version: '1.0.0',
+ author: 'EL',
logo: TAOLUSM_LOGO,
icon: TAOLUSM_LOGO,
site: TAOLUSM_DEFAULT_BASE,
@@ -71,7 +71,6 @@ function getManifest() {
}
async function getHome(ctx) {
- ctx = normalizeRuntimeContext(ctx);
let latestItems = [];
try {
const html = await fetchText(ctx, categoryURL(ctx, '/'));
@@ -111,7 +110,6 @@ async function getHome(ctx) {
}
async function getHomeSection(ctx) {
- ctx = normalizeRuntimeContext(ctx);
const sectionId = stringValue(ctx && (ctx.sectionId || ctx.id));
const section = findSection(sectionId) || TAOLUSM_SECTIONS[0];
try {
@@ -130,7 +128,6 @@ async function getHomeSection(ctx) {
}
async function getCategory(ctx) {
- ctx = normalizeRuntimeContext(ctx);
const page = positiveInt(contextValue(ctx, 'page'), 1);
const pageId = normalizePageId(ctx && (ctx.pageId || ctx.id || ctx.category || ctx.genreId));
const section = findSection(pageId) || TAOLUSM_SECTIONS[0];
@@ -151,7 +148,6 @@ async function getCategory(ctx) {
}
async function getDetail(ctx) {
- ctx = normalizeRuntimeContext(ctx);
const id = itemIdFromContext(ctx);
if (!id) throw new Error('套路SM 详情参数无效');
@@ -176,7 +172,6 @@ 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,
@@ -188,8 +183,8 @@ async function getDetail(ctx) {
posterHeaders: headers,
backdropHeaders: headers,
detailImageAspectRatio: '16:9',
- overview: [source.preview ? '公开预览;完整影片需要站点登录及观看权限。' : '', overview].filter(Boolean).join('\n'),
- resourceGroups: resourceGroupsFor(ctx, id, title, source),
+ overview: overview,
+ resourceGroups: resourceGroupsFor(ctx, id, title),
recommendations: recommendations.length ? [
{
id: 'related',
@@ -201,48 +196,27 @@ async function getDetail(ctx) {
};
}
-async function getResourceVersions(ctx) {
- ctx = normalizeRuntimeContext(ctx);
+function getResourceVersions(ctx) {
const id = itemIdFromContext(ctx);
if (!id) return [];
- 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);
+ return resourceGroupsFor(ctx, id, stringValue(ctx && ctx.title) || '在线播放');
}
-async function resolvePlayback(ctx) {
- ctx = normalizeRuntimeContext(ctx);
+function resolvePlayback(ctx) {
const id = itemIdFromContext(ctx);
- 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' };
-}
+ const url = stringValue(ctx && ctx.url) || (id ? downloadURL(ctx, id) : '');
+ if (!url) throw new Error('套路SM 播放参数无效');
-function parsePlaybackSource(html, id) {
- const scripts = String(html || '').match(/';
- c.discoverAvailableQualities = async () => [{ height: 1080, url: 'https://cdn.test/1080.m3u8' }, { height: 720, url: 'https://cdn.test/720.m3u8' }];
- c.extractPlayableFromLinkedPlayers = async () => { linked++; return ''; }; c.extractFromBrowser = async () => { browser++; return ''; };
- const result = await c.resolvePlayback({ itemId: c.makeItemId('https://missav.ws/cn/a', 'A', '') });
- assert.equal(result.url, 'https://cdn.test/1080.m3u8'); assert.equal(linked, 0); assert.equal(browser, 0);
-});
-test('MissAV accepts the original 1.0.7 detail payload and all legacy parameter names', () => {
- const c = load('missav-mini-library-download-working 6.js');
- const old = 'missav://detail?url=https%3A%2F%2Fmissav.ws%2Fdm247%2Fcn%2Fsample&title=Old';
- assert.equal(c.detailUrlFromContext({ itemId: old }), 'https://missav.ws/dm247/cn/sample');
- const names = c.getManifest().parameters.map(p => p.name);
- for (const name of ['baseURL', 'entryPath', 'backupBaseURLs', 'enableBrowserFallback', 'browserVisible', 'requestTimeoutSeconds', 'cacheMinutes']) assert.ok(names.includes(name));
-});
-
-const missavFile = 'missav-mini-library-download-working 6.js';
-const missavPage = 'Sample - MissAV' +
- '' +
- '';
-function missavInput(c) { return { itemId: c.makeItemId('https://missav.ws/cn/sample-001', 'Sample', '') }; }
-function acceleratedTimers() { return { setTimeout: (fn, ms) => setTimeout(fn, ms / 100) }; }
-
-test('MissAV HTTP 200 passive Cloudflare detection is not a blocking challenge', async () => {
- const c = load(missavFile); const requests = []; let browser = 0;
- c.Widget.http.get = async url => { requests.push(url); return { statusCode: 200, data: url.includes('.m3u8') ? master : missavPage }; };
- c.Widget.browser = { async fetch() { browser++; throw new Error('browser must not be needed'); } };
- const result = await c.resolvePlayback(missavInput(c));
- assert.equal(result.url, 'https://cdn.test/1080.m3u8');
- assert.equal(browser, 0);
- assert.deepEqual(requests, ['https://missav.ws/cn/sample-001', 'https://cdn.test/playlist.m3u8']);
- assert.equal(c.isCloudflare(missavPage, 200, { server: 'cloudflare' }), false);
- for (const [html, status, headers] of [
- ['Just a moment...Verify you are human', 200, {}],
- [missavPage, 200, { 'Cf-Mitigated': 'challenge' }], [missavPage, 403, {}]
- ]) assert.equal(c.isUsableHTML(html, status, headers), false);
-});
-test('MissAV uses its one browser call for media capture after a real challenge', async () => {
- const c = load(missavFile); const browserCalls = [], requests = [];
- c.Widget.http.get = async url => { requests.push(url); return { statusCode: 200, data: url.includes('.m3u8') ? master : 'Just a moment...Enable JavaScript and cookies to continue' }; };
- c.Widget.browser = { async fetch(url, options) { browserCalls.push({ url, options }); return { mediaSources: [{ url: 'https://cdn.test/playlist.m3u8' }] }; } };
- assert.equal((await c.resolvePlayback(missavInput(c))).url, 'https://cdn.test/1080.m3u8');
- assert.equal(browserCalls.length, 1); assert.equal(browserCalls[0].options.captureMedia, true);
- assert.equal(browserCalls[0].options.visible, false); assert.equal(requests.length, 2);
-});
-test('MissAV a host that ignores HTTP timeout still reaches media capture before the total deadline', async () => {
- const c = load(missavFile, acceleratedTimers()); let browser = 0, requests = 0;
- c.Widget.http.get = async url => { requests++; return url.includes('.m3u8') ? { status: 200, data: master } : new Promise(() => {}); };
- c.Widget.browser = { async fetch() { browser++; return { mediaSources: [{ url: 'https://cdn.test/playlist.m3u8' }] }; } };
- assert.equal((await c.resolvePlayback(missavInput(c))).url, 'https://cdn.test/1080.m3u8');
- assert.equal(browser, 1); assert.equal(requests, 2);
-});
-test('MissAV a hung browser reports the failing media stage without retrying mirrors', async () => {
- const c = load(missavFile, acceleratedTimers()); let browser = 0, requests = 0;
- c.Widget.http.get = async () => { requests++; throw new Error('offline'); };
- c.Widget.browser = { async fetch() { browser++; return new Promise(() => {}); } };
- await assert.rejects(c.resolvePlayback(missavInput(c)), error => /stage=browser-media/.test(error.message) && !/total-deadline/.test(error.message));
- assert.equal(browser, 1); assert.equal(requests, 1);
-});
-test('MissAV a hung optional master probe hands off the highest HTML quality without another browser', async () => {
- const c = load(missavFile, acceleratedTimers()); let browser = 0;
- c.Widget.http.get = async url => url.includes('.m3u8') ? new Promise(() => {}) : { status: 200, data: missavPage };
- c.Widget.browser = { async fetch() { browser++; return new Promise(() => {}); } };
- assert.equal((await c.resolvePlayback(missavInput(c))).url, 'https://cdn.test/1080p/video.m3u8');
- assert.equal(browser, 0);
-});
-test('MissAV playback refreshes detail HTML instead of using cached signed source URLs', async () => {
- const c = load(missavFile); let refreshes = 0;
- c.setCachedText({}, 'https://missav.ws/cn/sample-001', missavPage);
- c.Widget.http.get = async url => { if (url.includes('.m3u8')) return { status: 200, data: media }; refreshes++; return { status: 200, data: missavPage.replaceAll('https://cdn.test/', 'https://fresh.test/' + refreshes + '/') }; };
- assert.equal((await c.resolvePlayback(missavInput(c))).url, 'https://fresh.test/1/1080p/video.m3u8');
- assert.equal((await c.resolvePlayback(missavInput(c))).url, 'https://fresh.test/2/1080p/video.m3u8');
- assert.equal(refreshes, 2);
-});
-test('MissAV optional master discovery reserves time to hand off an already discovered stream', async () => {
- let offset = 0, masterTimeout;
- class PlaybackClock extends Date { static now() { return Date.now() + offset; } }
- const c = load(missavFile, { ...acceleratedTimers(), Date: PlaybackClock });
- c.Widget.http.get = async (url, options) => {
- if (url.includes('.m3u8')) { masterTimeout = options.timeoutSeconds; return new Promise(() => {}); }
- offset = 26500; return { status: 200, data: missavPage };
- };
- assert.equal((await c.resolvePlayback(missavInput(c))).url, 'https://cdn.test/1080p/video.m3u8');
- assert.ok(masterTimeout > 0 && masterTimeout <= 1);
-});
-test('MissAV resource discovery failure does not become a fake playable webpage', async () => {
- const c = load('missav-mini-library-download-working 6.js');
- c.fetchText = async () => { throw new Error('503'); }; c.extractFromBrowser = async () => '';
- const groups = await c.getResourceVersions({ itemId: c.makeItemId('https://missav.ws/cn/a', 'A', '') });
- assert.equal(Array.isArray(groups) ? groups.length : groups.groups?.length || 0, 0);
-});
-for (const file of ['missav-mini-library-download-working 6.js', 'sexbjcam-mini-library.js', 'javgg-mini-library.js', 'novipnoad-mini-library.js']) {
+for (const file of ['novipnoad-mini-library.js']) {
test(`${file}: a hung resolver has one overall deadline`, async () => {
const c = load(file, { setTimeout: (fn) => setTimeout(fn, 15) });
c.resolvePlaybackWithinBudget = async () => new Promise(() => {});
@@ -237,78 +147,6 @@ test('Manko keeps successful navigation cards when one category fails', async ()
c.primaryNavCard = async definition => { active++; max = Math.max(max, active); await Promise.resolve(); active--; if (definition[0] === 'new') throw new Error('offline'); return { id: definition[0], poster: 'https://img.test/a.jpg' }; };
const section = await c.getHomeSection({ sectionId: 'primary-nav' }); assert.ok(section.items.length > 0); assert.ok(max <= 2);
});
-test('ASMRLIB category previews come from their own category and load after home', async () => {
- const c = load('asmrlib-mini-library.js'); c.asmrHTTP = async () => '';
- c.asmrParseList = () => [{ id: 'home', poster: 'https://img.test/home.jpg' }];
- const home = await c.getHome(); assert.equal(home.sections[1].lazy, true);
- const requested = []; c.getCategory = async ({ pageId }) => { requested.push(pageId); return { items: [0,1,2].map(i => ({ id: `${pageId}-${i}`, poster: `https://img.test/${pageId}-${i}.jpg` })) }; };
- 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 = 'ASMR sample
';
-test('ASMRLIB current embedded players expose one interactive page action consistently', async () => {
- const c = load('asmrlib-mini-library.js');
- c.Widget.http.get = async () => ({ status: 200, data: asmrCurrentPage });
- const input = { itemId: 'asmrlib://post/' + asmrPost };
- const detail = await c.getDetail(input), groups = await c.getResourceVersions(input);
- assert.deepEqual(plain(groups), plain(detail.resourceGroups));
- const versions = groups[0].versions;
- assert.equal(versions.length, 1); assert.equal(versions[0].default, true);
- assert.equal(versions[0].action.itemId, input.itemId);
- assert.equal(c.asmrId(versions[0].action.versionId), asmrPost);
- assert.match(versions[0].subtitle, /验证或播放/);
-});
-for (const player of ['https://bysetayico.com/e/old', 'https://abyssplayer.com/old']) {
- test(`ASMRLIB legacy ${new URL(player).host} action preserves the original embedding page`, async () => {
- const c = load('asmrlib-mini-library.js'); const calls = []; let http = 0;
- c.Widget.http.get = async () => { http++; throw new Error('unnecessary detail refresh'); };
- c.Widget.browser = { async fetch(url, options) { calls.push({ url, options }); return { mediaSources: [{ url: 'https://cdn.test/main.m3u8', requestHeaders: { referer: player, origin: new URL(player).origin } }] }; } };
- const input = { itemId: 'asmrlib://post/' + asmrPost, versionId: 'asmrlib-line://old/' + encodeURIComponent(player) };
- const result = await c.resolvePlayback(JSON.stringify(input));
- assert.equal(result.url, 'https://cdn.test/main.m3u8'); assert.equal(result.container, 'm3u8');
- assert.equal(calls.length, 1); assert.equal(http, 0);
- assert.equal(calls[0].url, 'https://asmrlib.com/posts/' + asmrPost); assert.equal(calls[0].options.visible, true);
- assert.equal(calls[0].options.waitForMediaSource, true);
- assert.equal(calls[0].options.captureRequests, undefined); assert.equal(calls[0].options.captureMedia, undefined);
- assert.equal(result.headers.Referer, player); assert.equal(result.headers.Origin, new URL(player).origin);
- });
-}
-test('ASMRLIB page-version-only input uses captured media headers and not outer document headers', async () => {
- const c = load('asmrlib-mini-library.js');
- c.Widget.browser = { async fetch() { return { headers: { Origin: 'https://outer.test' }, capturedRequests: [{ url: 'https://cdn.test/final.mp4', headers: { Referer: 'https://player.test/', Origin: 'https://player.test', Cookie: 'verified=sample', Connection: 'keep-alive' } }] }; } };
- const result = await c.resolvePlayback({ versionId: 'asmrlib-page://post/' + asmrPost });
- assert.equal(result.url, 'https://cdn.test/final.mp4'); assert.equal(result.headers.Origin, 'https://player.test');
- assert.equal(result.headers.Cookie, 'verified=sample'); assert.equal(result.headers.Connection, undefined);
-});
-test('ASMRLIB refuses blob-only results and records capability evidence without retrying players', async () => {
- const c = load('asmrlib-mini-library.js'); let calls = 0;
- c.Widget.browser = { async fetch() { calls++; return { html: '', mediaSources: [{ url: 'blob:https://player.test/id' }] }; } };
- await assert.rejects(c.resolvePlayback({ itemId: asmrPost }), error => /stage=page-media/.test(error.message) && /blobOnly=true/.test(error.message) && /keys=html,mediaSources/.test(error.message));
- assert.equal(calls, 1);
-});
-test('ASMRLIB honors hidden-page preference and reports manual verification requirements', async () => {
- const c = load('asmrlib-mini-library.js'); let visible;
- c.Widget.browser = { async fetch(url, options) { visible = options.visible; return { html: '点击播放按钮以验证你是真人
' }; } };
- 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');
@@ -362,12 +200,6 @@ for (const mode of ['expired', 'rejected']) {
});
}
-test('MissAV playback budget owns browser fallback and preserves attached cookies', () => {
- const c = load('missav-mini-library-download-working 6.js');
- const options = c.requestOptions({__playbackBudget:{endAt:Date.now()+2000,expired:false}},'https://missav.ws/');
- assert.ok(options.timeout > 0 && options.timeout <= 2); assert.equal(options.useBrowserFallback,false); assert.equal(options.attachBrowserCookie,true);
-});
-
test('Novip playback awaits native browser media and caps both HTTP and browser stages', async () => {
const c = load('novipnoad-mini-library.js'); const calls=[];
c.parsePlayInfo = () => ({vid:'video123',pkey:'key'}); c.parseEpisodes = () => [];
@@ -396,367 +228,78 @@ test('XVideos attaches the configured session token without printing it', async
assert.equal(options.headers.Cookie,'session_token='+token); assert.ok(logs.every(line=>!line.includes(token)));
});
-const madouCard = id => ``;
-const taoluPage = (id, source, preview = true) => `Film ${id}
${preview ? '正在播放预览,VIP可免费观看完整视频' : ''}`;
-
-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/'}),/没有可播放媒体/);
-});
-
-function javggDetail(lines = ['VH', 'playmate', 'luluvdoo', 'SW']) {
- return '' + lines.map((line, i) =>
- `- Server${line}
`).join('') + '
' + lines.map((line, i) =>
- `
`).join('') + '
';
-}
-function packedJavgg(host = 'cdn', file = 'master', signature = 'fresh') {
- return `eval(function(p,a,c,k,e,d){return p;}('0({1:[{2:"3://4.5/6.7?8=9"}]});',36,10,'setup|sources|file|https|${host}|test|${file}|m3u8|sign|${signature}'.split('|'),0,{}))`;
-}
-
-test('JAVGG maps real server labels to numbered frames, despite missing/reordered frames', () => {
- const c = load('javgg-mini-library.js');
- const source = javggDetail().replace(/[\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);
-});
-
-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,{}))`;
+// These entrypoints intentionally restore their original uploaded bytes and contracts.
+// Recovery behavior added after those uploads is no longer part of this release.
+const firstVersions = JSON.parse(fs.readFileSync(path.join(root, 'tools/library-first-versions.json'), 'utf8'));
+const restoredIds = {
+ 'missav-mini-library.js': 'missav-mini-library',
+ 'sexbjcam-mini-library.js': 'sexbjcam-mini-library',
+ 'asmrlib-mini-library.js': 'asmrlib',
+ 'javgg-mini-library.js': 'javgg-mini-library',
+ 'madou8-mini-library 5.js': 'madou8-mini-library',
+ 'taolusm-mini-library.js': 'taolusm-mini-library',
+ 'kbjfan-mini-library.js': 'kbjfan-mini-library'
+};
+assert.deepEqual(Object.keys(firstVersions).sort(), Object.keys(restoredIds).sort());
+for (const [file, original] of Object.entries(firstVersions)) {
+ test(`${file}: bytes match the recorded first upload`, () => {
+ assert.match(original.commit, /^[0-9a-f]{40}$/);
+ assert.ok(original.sourcePath.endsWith('.js'));
+ const bytes = fs.readFileSync(path.join(root, file));
+ assert.equal(crypto.createHash('sha256').update(bytes).digest('hex'), original.sha256);
+ });
+ test(`${file}: restored manifest and public entrypoints retain the first-upload contract`, () => {
+ const c = load(file);
+ const manifest = c.getManifest();
+ assert.equal(manifest.id, restoredIds[file]);
+ assert.equal(manifest.version, original.version);
+ assert.ok(manifest.name && manifest.title);
+ assert.equal(manifest.aggregation.search, true);
+ for (const entry of ['getHome', 'getHomeSection', 'getCategory', 'getDetail', 'getResourceVersions', 'resolvePlayback', 'search']) {
+ assert.equal(typeof c[entry], 'function', entry);
+ }
+ });
}
-
-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');
+test('MissAV legacy download paths contain the same first-upload implementation', () => {
+ const bytes = fs.readFileSync(path.join(root, 'missav-mini-library.js'));
+ for (const file of ['missav-mini-library-CloudFlare.js', 'missav-mini-library-download-working.js', 'missav-mini-library-download-working 6.js']) {
+ assert.deepEqual(fs.readFileSync(path.join(root, file)), bytes, file);
}
- 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('MissAV first upload retains direct HLS playback and its detail identifier', async () => {
+ const c = load('missav-mini-library.js'); let requests = 0;
+ c.Widget.http.get = async () => { requests++; throw new Error('direct playback should not fetch detail'); };
+ const itemId = c.makeItemId('https://missav.ws/cn/sample', 'Sample', '');
+ assert.equal(c.detailUrlFromContext({ itemId }), 'https://missav.ws/cn/sample');
+ const playback = await c.resolvePlayback({ itemId, url: 'https://cdn.test/original.m3u8' });
+ assert.equal(playback.url, 'https://cdn.test/original.m3u8');
+ assert.equal(playback.container, 'm3u8'); assert.equal(requests, 0);
});
-
-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/);
+test('ASMRLIB first upload retains separate embedded player lines and their version identifiers', async () => {
+ const c = load('asmrlib-mini-library.js');
+ const post = 'becd2651e7d56ca656d27766d036dcee';
+ c.Widget.http.get = async () => ({ data: '
Sample
' });
+ const detail = await c.getDetail({ itemId: 'asmrlib://post/' + post });
+ const groups = await c.getResourceVersions({ itemId: 'asmrlib://post/' + post });
+ assert.deepEqual(plain(groups), plain(detail.resourceGroups));
+ const versions = groups[0].versions;
+ assert.deepEqual(plain(versions.map(x => x.name)), ['BI 线路', 'AB 线路']);
+ assert.deepEqual(plain(versions.map(x => c.asmrVersionPlayerURL(x.action))), ['https://bysetayico.com/e/sample', 'https://abyssplayer.com/sample']);
+ assert.equal(versions[0].default, true); assert.equal(versions[1].default, false);
+});
+test('KBJ first upload object search retains the configured domain and page', async () => {
+ const c = load('kbjfan-mini-library.js'); const requests = [];
+ c.Widget.http.get = async url => { requests.push(url); return { data: '' }; };
+ const result = await c.search({ query: 'A&B #tag', page: 2, params: { baseURL: 'https://kbj.test' } });
+ assert.deepEqual(requests, ['https://kbj.test/?s=A%26B%20%23tag&paged=2']);
+ assert.equal(result.pageType, 'search'); assert.equal(result.page, 2);
+});
+test('Taolu first upload retains its download version and original playback gateway', () => {
+ const c = load('taolusm-mini-library.js');
+ const groups = c.getResourceVersions({ itemId: '121576' });
+ assert.equal(groups[0].versions[0].id, 'download-121576');
+ assert.equal(groups[0].versions[0].url, 'https://taolusm.com/download/121576');
+ const playback = c.resolvePlayback({ itemId: '121576' });
+ assert.equal(playback.url, groups[0].versions[0].url);
+ assert.equal(playback.container, 'mp4');
});
diff --git a/tools/library-contracts.json b/tools/library-contracts.json
index bd692d4..919990a 100644
--- a/tools/library-contracts.json
+++ b/tools/library-contracts.json
@@ -278,12 +278,7 @@
"id": "missav-mini-library",
"parameters": [
"baseURL",
- "entryPath",
- "backupBaseURLs",
- "enableBrowserFallback",
- "browserVisible",
- "requestTimeoutSeconds",
- "cacheMinutes"
+ "entryPath"
],
"entryPoints": [
"getManifest",
@@ -301,15 +296,7 @@
"id": "missav-mini-library",
"parameters": [
"baseURL",
- "entryPath",
- "backupBaseURLs",
- "enableBrowserFallback",
- "browserVisible",
- "automaticBrowserFallback",
- "verificationCooldownMinutes",
- "requestTimeoutSeconds",
- "cacheMinutes",
- "categoryRestoreMinutes"
+ "entryPath"
],
"entryPoints": [
"getManifest",
@@ -327,15 +314,7 @@
"id": "missav-mini-library",
"parameters": [
"baseURL",
- "entryPath",
- "backupBaseURLs",
- "enableBrowserFallback",
- "browserVisible",
- "automaticBrowserFallback",
- "verificationCooldownMinutes",
- "requestTimeoutSeconds",
- "cacheMinutes",
- "categoryRestoreMinutes"
+ "entryPath"
],
"entryPoints": [
"getManifest",
diff --git a/tools/library-first-versions.json b/tools/library-first-versions.json
new file mode 100644
index 0000000..5d6a64b
--- /dev/null
+++ b/tools/library-first-versions.json
@@ -0,0 +1,44 @@
+{
+ "missav-mini-library.js": {
+ "commit": "a44d25631a06c29b4491610c96c965bb03c9711d",
+ "version": "1.0.0",
+ "sha256": "5fa423c1762260c0ad39f50a7581b22b5036442e2021b593c3ab114afc2e9b53",
+ "sourcePath": "missav-mini-library.js"
+ },
+ "sexbjcam-mini-library.js": {
+ "commit": "420f4fa2a5812cb647249a184f59696cbbdb02fb",
+ "version": "1.1.3",
+ "sha256": "7b682e9a1791655dab2ccfc14aafa736c1e82026a21bf39125b3e251a41383a5",
+ "sourcePath": "sexbjcam-mini-library.js"
+ },
+ "asmrlib-mini-library.js": {
+ "commit": "4a2ece99e4af217c1509c522c0d84d276b068b08",
+ "version": "1.2.0",
+ "sha256": "2cb404dbbab67626d762248dbe050ab7e534ea68278ff745c5d304b6a487faa0",
+ "sourcePath": "asmrlib-mini-library.js"
+ },
+ "javgg-mini-library.js": {
+ "commit": "b17286ba023d392cf9a6e3935590e2bc5c528b06",
+ "version": "1.0.0",
+ "sha256": "39092a7afb7107182c33fc923d93cb99c20ea3fb97d01810ef1f4e974de8885e",
+ "sourcePath": "javgg-mini-library.js"
+ },
+ "madou8-mini-library 5.js": {
+ "commit": "2a16937466dfbb908ae1bab6a1347569299df1df",
+ "sourcePath": "madou8-mini-library.js",
+ "version": "1.0.1",
+ "sha256": "806590d98c709021d198d6720c396bce0a8bdaeb8415475841384fb44cde83bf"
+ },
+ "taolusm-mini-library.js": {
+ "commit": "fbb65e110e60561c4a557adb42c80dc2e173505f",
+ "version": "1.0.0",
+ "sha256": "04646ae4b033e37719291cb196bcecb11b9c0917d065eb29b97cf4e15c9ddc8f",
+ "sourcePath": "taolusm-mini-library.js"
+ },
+ "kbjfan-mini-library.js": {
+ "commit": "e943d616e2df05c3abf6fdf2a716780c9d6a0aff",
+ "version": "1.0.0",
+ "sha256": "761d79a9a4e96ced1484bede83d123dc6cc9af5cc94f13fdc49bc26f84b810e1",
+ "sourcePath": "kbjfan-mini-library.js"
+ }
+}