From e8330ec79cb586778b09c655adeece62c9ec8144 Mon Sep 17 00:00:00 2001 From: hzl <1803573449@qq.com> Date: Sat, 3 Oct 2026 17:50:45 +0800 Subject: [PATCH] Optimize media scripts and preserve imported download paths --- .github/workflows/validate-scripts.yml | 37 + .gitignore | 6 + 123av-mini-library.js | 22 +- 1808-mini-library.js | 151 +- 4kvm-dreamby-mini-library.js | 18 +- 591av-mini-library.js | 52 +- 91porny_int.js | 4 +- CHANGELOG.md | 35 + MissAV 3.0.js | 27 +- README.md | 75 + asmrlib-mini-library.js | 35 +- girigirilove-mini-library.js | 107 +- jable.js | 14 +- jable.media-library.js | 88 +- javgg-mini-library.js | 47 +- kbjfan-mini-library.js | 24 +- madou8-domestic-mini-library.js | 21 +- madou8-mini-library 5.js | 21 +- manko-fun-mini-library.js | 14 +- missav-mini-library-CloudFlare.js | 1581 +++++++++++++++-- missav-mini-library-download-working 6.js | 90 +- missav-mini-library-download-working.js | 90 +- missav-mini-library.js | 1916 +++++++++++++++++++-- novipnoad-mini-library.js | 67 +- pornhub.media-library.js | 10 +- pornhub_int.js | 4 +- sexbjcam-mini-library.js | 53 +- sexbjcam-network-recovery.test.js | 5 + stripchat-angellive/worker/README.md | 6 +- stripchat-angellive/worker/index.js | 130 +- stripchat-angellive/worker/test.js | 77 + stripchat-angellive/worker/wrangler.toml | 4 + taolusm-mini-library.js | 24 +- tests/adapters.test.cjs | 259 +++ tools/commit-library-aliases.cjs | 19 + tools/library-aliases.json | 9 + tools/library-contracts.json | 636 +++++++ tools/sync-library-aliases.cjs | 25 + tools/verify-release.py | 34 + tools/verify-scripts.cjs | 36 + xvideos_int.js | 3 +- xxxfollow-mini-library 5.js | 84 +- xxxfollow-mini-library.js | 84 +- 43 files changed, 5247 insertions(+), 797 deletions(-) create mode 100644 .github/workflows/validate-scripts.yml create mode 100644 .gitignore create mode 100644 CHANGELOG.md create mode 100644 README.md create mode 100644 tests/adapters.test.cjs create mode 100644 tools/commit-library-aliases.cjs create mode 100644 tools/library-aliases.json create mode 100644 tools/library-contracts.json create mode 100644 tools/sync-library-aliases.cjs create mode 100644 tools/verify-release.py create mode 100644 tools/verify-scripts.cjs diff --git a/.github/workflows/validate-scripts.yml b/.github/workflows/validate-scripts.yml new file mode 100644 index 0000000..d7daf4f --- /dev/null +++ b/.github/workflows/validate-scripts.yml @@ -0,0 +1,37 @@ +name: Validate scripts and import compatibility +on: + push: + pull_request: +permissions: + contents: read +jobs: + validate: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + - if: github.event_name == 'push' + run: node tools/sync-library-aliases.cjs + - run: node tools/sync-library-aliases.cjs --check + - run: node tools/verify-scripts.cjs + - run: node --test tests/adapters.test.cjs + - run: node sexbjcam-network-recovery.test.js + - run: node stripchat-angellive/test.js + - run: node --test stripchat-angellive/worker/test.js + - run: python3 tools/verify-release.py + - run: git diff --check + sync-compatibility-paths: + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + needs: validate + runs-on: ubuntu-24.04 + permissions: + contents: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '24' + - run: node tools/sync-library-aliases.cjs + - run: node tools/commit-library-aliases.cjs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4f7897a --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +node_modules/ +.wrangler/ +.env +.env.* +.dev.vars +.DS_Store diff --git a/123av-mini-library.js b/123av-mini-library.js index 1aa277e..94c16f6 100644 --- a/123av-mini-library.js +++ b/123av-mini-library.js @@ -11,7 +11,7 @@ const WidgetMetadata = { id: '123av-mini-library', name: '123AV', title: '123AV', - version: '1.1.0', + version: '1.1.1', requiredVersion: '0.0.1', author: 'Alan huang', site: AV123_DEFAULT_BASE, @@ -93,6 +93,7 @@ function getManifest() { } async function getHome(ctx) { + ctx = normalizeRuntimeContext(ctx); const browseSection = { id: '123av-browse', title: '分类浏览', @@ -116,6 +117,7 @@ async function getHome(ctx) { } async function getHomeSection(ctx) { + ctx = normalizeRuntimeContext(ctx); const sectionId = stringValue(ctx && (ctx.sectionId || ctx.id || ctx.pageId)); const section = findSection(sectionId) || AV123_SECTIONS[0]; try { @@ -134,6 +136,7 @@ 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), 'new'); const section = findSection(pageId); @@ -160,6 +163,7 @@ async function getCategory(ctx) { } async function getDetail(ctx) { + ctx = normalizeRuntimeContext(ctx); const detailUrl = detailURLFromContext(ctx); if (!detailUrl) throw new Error('123AV 详情参数无效'); @@ -240,6 +244,7 @@ async function getDetail(ctx) { } async function getResourceVersions(ctx) { + ctx = normalizeRuntimeContext(ctx); const direct = stringValue(ctx && (ctx.playUrl || ctx.url || ctx.videoUrl)); if (isPlayableURL(direct)) { return playbackGroups('', ctx && ctx.title, ctx && ctx.surritCode, ctx && ctx.playerUrl, direct, ctx); @@ -256,6 +261,7 @@ async function getResourceVersions(ctx) { } async function resolvePlayback(ctx) { + ctx = normalizeRuntimeContext(ctx); const decoded = decodeDetailPayload(ctx && (ctx.versionId || ctx.itemId || ctx.id)); const direct = firstNonEmpty(ctx && ctx.playUrl, ctx && ctx.url, ctx && ctx.videoUrl, decoded.playUrl); if (isPlayableURL(direct) && !isDetailURL(direct)) { @@ -278,6 +284,7 @@ async function resolvePlayback(ctx) { } async function search(ctx) { + ctx = normalizeRuntimeContext(ctx); const query = stringValue(ctx && (ctx.query || ctx.keyword || ctx.text)); const page = positiveInt(contextValue(ctx, 'page'), 1); if (!query) { @@ -993,3 +1000,16 @@ if (typeof globalThis !== 'undefined') { if (typeof module !== 'undefined' && module.exports) { module.exports = exported; } + +function normalizeRuntimeContext(input) { + let ctx = input; + if (typeof ctx === 'string') { try { ctx = JSON.parse(ctx); } catch (_) { ctx = {}; } } + if (!ctx || typeof ctx !== 'object' || Array.isArray(ctx)) return {}; + const nested = {}; + ['params', 'config', 'settings', 'parameters', 'pagination', 'pageInfo'].forEach(function (key) { + let bag = ctx[key]; + if (typeof bag === 'string') { try { bag = JSON.parse(bag); } catch (_) { bag = null; } } + if (bag && typeof bag === 'object' && !Array.isArray(bag)) Object.keys(bag).forEach(function (name) { if (nested[name] === undefined) nested[name] = bag[name]; }); + }); + return Object.assign(nested, ctx); +} diff --git a/1808-mini-library.js b/1808-mini-library.js index 4ca6686..6088a5c 100644 --- a/1808-mini-library.js +++ b/1808-mini-library.js @@ -1,6 +1,6 @@ /** * 1808.online - Dreamby / baiPlay 自定义媒体库 - * 电脑端验证版本:1.0.5 + * 脚本版本:1.0.6(原地址兼容更新) */ const SITE = "https://1808.online"; @@ -109,8 +109,8 @@ async function fetchText(ctx, path, referer) { else if (typeof $http !== "undefined" && typeof $http.request === "function") response = await $http.request({ url, method: "GET", ...options }); else if (typeof fetch === "function") response = await fetch(url, { headers }); else throw new Error("当前环境没有可用的 HTTP 客户端"); - if (response && typeof response.text === "function") return response.text(); - const html = responseText(response); + if (Number(response && (response.status || response.statusCode)) >= 400) throw new Error('源站 HTTP ' + (response.status || response.statusCode)); + const html = response && typeof response.text === 'function' ? await response.text() : responseText(response); if (!html || /Just a moment|cf-mitigated|Cloudflare Ray ID/i.test(html)) throw new Error("站点返回空内容或 Cloudflare 验证页"); return html; } @@ -121,7 +121,7 @@ function imageHeaders(ctx, referer) { function fallbackItems(ctx) { return HOME_FALLBACK.map(([id, title, poster, year]) => ({ - id, title, subtitle: String(year), type: "movie", poster, year, + id, title, subtitle: "历史推荐(2026-07-18)", badges: ["历史片单"], type: "movie", poster, year, imageHeaders: imageHeaders(ctx), posterHeaders: imageHeaders(ctx), action: { type: "detail", itemId: id } })); @@ -147,31 +147,9 @@ function movieSlug(id) { return movieId(id).match(/\/movies\/([^/]+)\.html$/i)?.[1] || ""; } -function syntheticPlaybackLines(ctx, id) { - const slug = movieSlug(id); - if (!slug) return []; - const referer = absoluteURL(ctx, id); - const definitions = [ - ["标清 360P", `https://ru.youxijiasu007.shop/movie/360/${slug}/360.m3u8`], - ["高清 720P", `https://ru.youxijiasu007.shop/movie/720/${slug}/720.m3u8`], - ["标清 360P 线路2", `https://ru.kuikuigo8923.space/movie/360/${slug}/360.m3u8`], - ["高清 720P 线路2", `https://ru.kuikuigo8923.space/movie/720/${slug}/720.m3u8`], - ["标清 360P 移动线路", `https://fmv.youxijiasu007.shop/movie/360/${slug}/360.m3u8`], - ["高清 720P 移动线路", `https://fmv.youxijiasu007.shop/movie/720/${slug}/720.m3u8`] - ]; - return definitions.map(([title, url]) => ({ id: url, title, url, referer })); -} -function fallbackDetail(ctx, id) { - const seed = HOME_FALLBACK.find(item => item[0] === id); - const title = seed?.[1] || movieSlug(id).replace(/_/g, " "); - const poster = seed?.[2] || `https://img.9bf85763.website/covers/${movieSlug(id)}.jpg`; - return { - title, poster, overview: "", - actorText: "", urls: syntheticPlaybackLines(ctx, id), related: fallbackItems(ctx).filter(item => item.id !== id).slice(0, 12), - referer: absoluteURL(ctx, id) - }; -} + + function parseMovieCards(ctx, html) { const cards = []; @@ -226,7 +204,7 @@ function parseDetailPage(ctx, html, id) { function getManifest() { return { - id: "1808-online", name: "1808在线", title: "1808在线", version: "1.0.5", author: "Codex", + id: "1808-online", name: "1808在线", title: "1808在线", version: "1.0.6", author: "Alan huang", logo: ICON, icon: ICON, site: SITE, adult: true, capabilities: { home: true, category: true, detail: true, search: false, resourceVersions: true, playback: true, resourceMatching: false }, aggregation: { search: false, playbackHistory: true, resourceMatching: false }, @@ -240,78 +218,53 @@ async function loadCategory(ctx, id) { } async function getHome(ctx) { - let latest = fallbackItems(ctx); - if (hasHTTPClient()) { - try { - const remote = (await loadCategory(ctx, "latest")).items.slice(0, 18); - if (remote.length) latest = remote; - } catch (_) {} - } + let latest = []; let error = ''; + try { latest = (await loadCategory(ctx, 'latest')).items.slice(0, 18); if (!latest.length) throw new Error('最新片单为空'); } + catch (e) { error = String(e && e.message || e); latest = fallbackItems(ctx); } return { - pageType: "home", id: "1808-home", title: "1808在线", heroAspectRatio: "2:3", hero: latest.slice(0, 6), items: latest, - sections: [{ ...CATEGORIES[0], lazy: false, items: latest, moreAction: categoryMoreAction(CATEGORIES[0]) }] - .concat(CATEGORIES.slice(1).map(v => ({ - ...v, lazy: true, items: [], moreAction: categoryMoreAction(v), - loadAction: { type: "custom", sectionId: v.id, id: v.id, title: v.title } - }))) + pageType: 'home', id: '1808-home', title: '1808在线', heroAspectRatio: '2:3', hero: latest.slice(0, 6), items: latest, + sections: [{ ...CATEGORIES[0], title: error ? '历史推荐(2026-07-18,实时加载失败)' : CATEGORIES[0].title, + lazy: false, items: latest, error: error || undefined, moreAction: categoryMoreAction(CATEGORIES[0]) }] + .concat(CATEGORIES.slice(1).map(v => ({ ...v, lazy: true, items: [], moreAction: categoryMoreAction(v), + loadAction: { type: 'custom', sectionId: v.id, id: v.id, title: v.title } }))) }; } async function getHomeSection(ctx) { - const id = String(value(ctx, ["sectionId", "pageId", "id"], "latest")); + const id = String(value(ctx, ['sectionId', 'pageId', 'id'], 'latest')); const category = CATEGORIES.find(v => v.id === id) || CATEGORIES[0]; - let items = fallbackItems(ctx); - if (hasHTTPClient()) { - try { - const remote = (await loadCategory(ctx, category.id)).items.slice(0, 18); - if (remote.length) items = remote; - } catch (_) {} - } - return { - id: category.id, title: category.title, style: category.style, lazy: false, items, - moreAction: categoryMoreAction(category) - }; + let items = []; let error = ''; + try { items = (await loadCategory(ctx, category.id)).items.slice(0, 18); } + catch (e) { error = String(e && e.message || e); if (category.id === 'latest') items = fallbackItems(ctx); } + return { id: category.id, title: error && items.length ? '历史推荐(2026-07-18,实时加载失败)' : category.title, + style: category.style, lazy: false, items, error: error || undefined, moreAction: categoryMoreAction(category) }; } async function getCategory(ctx) { - const id = String(value(ctx, ["pageId", "id"], "latest")); + const id = String(value(ctx, ['pageId', 'id'], 'latest')); const category = CATEGORIES.find(v => v.id === id) || CATEGORIES[0]; - let items = fallbackItems(ctx); - if (hasHTTPClient()) { - try { - const remote = (await loadCategory(ctx, category.id)).items; - if (remote.length) items = remote; - } catch (_) {} - } - return { pageType: "category", id: category.id, title: category.title, style: "media.posterGrid", itemAspectRatio: "2:3", page: 1, hasMore: false, items }; + let items = []; let error = ''; + try { items = (await loadCategory(ctx, category.id)).items; } catch (e) { error = String(e && e.message || e); } + return { pageType: 'category', id: category.id, title: category.title, style: 'media.posterGrid', itemAspectRatio: '2:3', page: 1, hasMore: false, items, error: error || undefined }; } async function getDetail(ctx) { - const id = movieId(value(ctx, ["itemId", "id", "sourceId", "path"], "")); - if (!id) throw new Error("缺少有效的电影详情地址"); - let detail = fallbackDetail(ctx, id); - if (hasHTTPClient()) { - try { - const remote = parseDetailPage(ctx, await fetchText(ctx, id), id); - if (!remote.urls.length) remote.urls = syntheticPlaybackLines(ctx, id); - detail = remote; - } catch (_) {} - } - if (!detail.urls.length) throw new Error("详情页没有解析到播放线路"); - const versions = detail.urls.map((v, index) => ({ id: v.url, title: v.title, name: v.title, url: v.url, container: "hls", default: index === 0, headers: playbackHeaders(v.referer), action: { type: "play", itemId: id, versionId: v.url, url: v.url, path: v.url, referer: v.referer, title: v.title } })); - return { - pageType: "detail", id, type: "movie", title: detail.title, poster: detail.poster, backdrop: detail.poster, - detailImageAspectRatio: "2:3", imageHeaders: imageHeaders(ctx, detail.referer), overview: detail.overview, - year: Number(detail.title.match(/(?:19|20)\d{2}/)?.[0]) || undefined, genres: ["剧情"], - cast: detail.actorText.split(/[,,、]/).filter(Boolean).slice(0, 20).map(name => ({ name: name.trim(), role: "演员" })), - resourceGroups: [{ id: "1808-lines", title: "播放线路", versions }], - recommendations: [{ id: "related", title: "相关推荐", style: "discover.posterCompact", items: detail.related }] - }; + const id = movieId(value(ctx, ['itemId', 'id', 'sourceId', 'path'], '')); + if (!id) throw new Error('缺少有效的电影详情地址'); + const detail = parseDetailPage(ctx, await fetchText(ctx, id), id); + if (!detail.urls.length) throw new Error('详情页没有返回真实播放线路,请刷新重试'); + const lines = detail.urls.slice().sort((a, b) => lineQuality(b) - lineQuality(a)); + const versions = lines.map((v, index) => ({ id: v.url, title: v.title, name: v.title, container: 'hls', default: index === 0, + headers: playbackHeaders(v.referer), action: { type: 'play', itemId: id, versionId: v.url, referer: v.referer, title: v.title } })); + return { pageType: 'detail', id, type: 'movie', title: detail.title, poster: detail.poster, backdrop: detail.poster, + detailImageAspectRatio: '2:3', imageHeaders: imageHeaders(ctx, detail.referer), overview: detail.overview, + year: Number(detail.title.match(/(?:19|20)\d{2}/)?.[0]) || undefined, genres: ['剧情'], + cast: detail.actorText.split(/[,,、]/).filter(Boolean).slice(0, 20).map(name => ({ name: name.trim(), role: '演员' })), + resourceGroups: [{ id: '1808-lines', title: '播放线路', versions }], + recommendations: [{ id: 'related', title: '相关推荐', style: 'discover.posterCompact', items: detail.related }] }; } async function getResourceVersions(ctx) { - const direct = String(value(ctx, ["versionId", "resourceId", "url", "playUrl", "videoUrl"], "")); - if (/^https?:\/\//i.test(direct)) return { groups: [{ id: "1808-lines", title: "播放线路", versions: [{ id: direct, title: "在线播放", url: direct, container: "hls" }] }] }; const detail = await getDetail(ctx); return { itemId: detail.id, groups: detail.resourceGroups }; } @@ -322,17 +275,25 @@ function playbackHeaders(referer) { async function resolvePlayback(ctx) { ctx = parseCtx(ctx); - let url = String(value(ctx, ["versionId", "resourceId", "url", "path", "playUrl", "videoUrl"], "")); - let referer = String(value(ctx, ["referer", "detailUrl"], "")); - if (!/^https?:\/\//i.test(url) || !/\.m3u8(?:\?|$)/i.test(url)) { - const id = movieId(value(ctx, ["itemId", "id", "sourceId"], "")); - if (!id) throw new Error("没有解析到电影 ID 或播放地址"); - const lines = syntheticPlaybackLines(ctx, id); - if (!lines.length) throw new Error("没有解析到播放地址"); - url = lines[0].url; - referer = lines[0].referer; + const id = movieId(value(ctx, ['itemId', 'id', 'sourceId'], '')); + const requested = String(value(ctx, ['versionId', 'resourceId', 'url', 'path', 'playUrl', 'videoUrl'], '')); + let url = requested; let referer = String(value(ctx, ['referer', 'detailUrl'], '')); + if (id) { + const detail = parseDetailPage(ctx, await fetchText(ctx, id), id); + const lines = detail.urls.slice().sort((a, b) => lineQuality(b) - lineQuality(a)); + const quality = lineQuality({ url: requested }); + const selected = lines.find(v => v.url === requested) || (quality && lines.find(v => lineQuality(v) === quality)) || lines[0]; + if (!selected) throw new Error('源站没有返回可播放线路,请刷新重试'); + url = selected.url; referer = selected.referer || detail.referer; } - return { url, container: "hls", headers: playbackHeaders(referer), startPositionSeconds: 0, isLive: false, streamKind: "vod" }; + if (!/^https?:\/\//i.test(url) || !/\.m3u8(?:[?#]|$)/i.test(url)) throw new Error('没有解析到真实 HLS 播放地址'); + if (!/#EXTM3U/.test(await fetchText(ctx, url, referer))) throw new Error('播放线路未返回有效 HLS 清单'); + return { url, container: 'hls', headers: playbackHeaders(referer), startPositionSeconds: 0, isLive: false, streamKind: 'vod' }; +} + +function lineQuality(line) { + const m = String(line.title || '').match(/(\d{3,4})\s*p/i) || String(line.url || '').match(/(?:^|\/)(\d{3,4})(?:p|\/)/i); + return m ? Number(m[1]) : 0; } const api = { diff --git a/4kvm-dreamby-mini-library.js b/4kvm-dreamby-mini-library.js index 512bc97..bb6b995 100644 --- a/4kvm-dreamby-mini-library.js +++ b/4kvm-dreamby-mini-library.js @@ -169,7 +169,7 @@ function kvmHomeGroupItems(html, id) { } function getManifest() { - return { id: '4kvm-dreamby', name: '4K影视', title: '4K影视', version: '1.10.0', author: 'Alan huang', logo: KVM_LOGO, + return { id: '4kvm-dreamby', name: '4K影视', title: '4K影视', version: '1.10.1', author: 'Alan huang', logo: KVM_LOGO, capabilities: { search: true, aggregation: true, playbackHistory: true }, aggregation: { search: true, playbackHistory: true } }; } const WidgetMetadata = getManifest(); @@ -225,8 +225,8 @@ function kvmHas4K(html) { return /(?:^|\s)4k\s*(?:加入片单|评分|收藏)/i.test(text) || /(?:badge|tag)[^>]*>\s*4k\s* !q.locked && /\.m3u8|\.mp4/i.test(q.url)); + const excluded = new Set(qualities.filter(q => q.locked).map(q => q.url)); + const isMain = url => !excluded.has(url) && !/(?:^|[\/_.-])(?:ads?|advert(?:isement)?|preroll|trailer|preview)(?:[\/_.-]|$)/i.test(url); + const playable = qualities.filter(q => !q.locked && /\.m3u8|\.mp4/i.test(q.url) && isMain(q.url)); const real1080 = playable.find(q => /1080|超清/i.test(q.title)); - const media1080 = media.find(url => /1080(?:p)?(?:[^0-9]|$)/i.test(url)); - const url = real1080 && real1080.url || media1080 || media[media.length - 1] || media[0]; + const validMedia = media.filter(isMain); + const media1080 = validMedia.find(url => /1080(?:p)?(?:[^0-9]|$)/i.test(url)); + const url = real1080 && real1080.url || media1080 || (playable.length === 1 ? playable[0].url : '') || (validMedia.length === 1 ? validMedia[0] : ''); + if (!url && validMedia.length > 1) throw new Error('4K影视播放:捕获到多个未明确归属的媒体地址,请重试;stage=media-selection'); if (!url) { const keys = result && typeof result === 'object' ? Object.keys(result).join(',') : typeof result; throw new Error('4K影视播放:浏览器未返回最终媒体地址;stage=media-capture;page=' + page + ';resultKeys=' + keys + ';mediaCandidates=0;blobOnly=true'); } const hls = /\.m3u8(?:[?#]|$)/i.test(url); // This source's HLS playlist and its segments live on different CDNs. The diff --git a/591av-mini-library.js b/591av-mini-library.js index fd168b8..4869b01 100644 --- a/591av-mini-library.js +++ b/591av-mini-library.js @@ -8,7 +8,7 @@ const WidgetMetadata = { id: '591av-mini-library', name: '591AV', title: '591AV', - version: '1.3.1', + version: '1.3.2', requiredVersion: '0.0.1', author: 'Alan huang', site: AV591_DEFAULT_BASE, @@ -81,8 +81,8 @@ async function getHome(ctx) { pageType: 'home', id: '591av-home', title: WidgetMetadata.title, heroAspectRatio: '16:9', hero: latest.slice(0, 6), sections: [{ - id: 'browse', title: '分类浏览', style: 'discover.annualPosterStack', lazy: false, - items: await buildBrowseCards(ctx, latest) + id: 'browse', title: '分类浏览', style: 'discover.annualPosterStack', lazy: true, + loadAction: { type: 'custom', id: 'browse', sectionId: 'browse', title: '分类浏览' }, items: [] }, { id: 'new', title: '最新上架', style: 'discover.spotlight', lazy: false, moreAction: categoryAction(AV591_SECTIONS[0]), items: latest, @@ -97,32 +97,32 @@ async function getHome(ctx) { }; } -async function buildBrowseCards(ctx, fallbackItems) { +async function buildBrowseCards(ctx) { const selected = AV591_BROWSE.slice(0, 6); - const cards = []; - for (let i = 0; i < selected.length; i += 1) { - const category = selected[i]; + const cards = await mapBrowseCards(selected, async function (category) { let previews = []; - try { - previews = parseCards(await fetchText(ctx, listingURL(ctx, category, 1, 'post_date')), ctx).slice(0, 3); - } catch (_) {} - if (!previews.length) continue; + try { previews = parseCards(await fetchText(ctx, listingURL(ctx, category, 1, 'post_date')), ctx).slice(0, 3); } catch (_) {} + if (!previews.length) return null; const lead = previews[0]; - cards.push({ - id: category.id, title: category.title, type: 'collection', - poster: lead.poster, backdrop: lead.backdrop || lead.poster, - imageHeaders: lead.imageHeaders, posterHeaders: lead.posterHeaders, - backdropHeaders: lead.backdropHeaders, previewItems: previews, - action: { - type: 'category', id: category.id, pageId: category.id, title: category.title, - page: 1, currentPage: 1, itemAspectRatio: '16:9' - } - }); - } - return cards; + return { id: category.id, title: category.title, type: 'collection', poster: lead.poster, backdrop: lead.backdrop || lead.poster, + imageHeaders: lead.imageHeaders, posterHeaders: lead.posterHeaders, backdropHeaders: lead.backdropHeaders, previewItems: previews, + action: { type: 'category', id: category.id, pageId: category.id, title: category.title, page: 1, currentPage: 1, itemAspectRatio: '16:9' } }; + }); + return cards.filter(Boolean); +} + +async function mapBrowseCards(items, load) { + const results = new Array(items.length); let next = 0; + await Promise.all(Array.from({ length: Math.min(2, items.length) }, async function () { + while (next < items.length) { const index = next++; results[index] = await load(items[index]); } + })); + return results; } async function getHomeSection(ctx) { + if (value(ctx, ['sectionId', 'pageId', 'id'], 'new') === 'browse') { + return { id: 'browse', title: '分类浏览', style: 'discover.annualPosterStack', lazy: false, items: await buildBrowseCards(ctx, []) }; + } const section = findSection(value(ctx, ['sectionId', 'pageId', 'id'], 'new')) || AV591_SECTIONS[0]; try { const items = parseCards(await fetchText(ctx, listingURL(ctx, section, 1, section.sort || 'post_date')), ctx).slice(0, 20); @@ -145,8 +145,8 @@ async function getCategory(ctx) { try { const sourcePage = (page - 1) * 2 + 1; const payloads = await Promise.all([ - fetchText(ctx, listingURL(ctx, category, sourcePage, sort, true)).catch(function () { return ''; }), - fetchText(ctx, listingURL(ctx, category, sourcePage + 1, sort, true)).catch(function () { return ''; }) + fetchText(ctx, listingURL(ctx, category, sourcePage, sort, true)), + fetchText(ctx, listingURL(ctx, category, sourcePage + 1, sort, true)) ]); const firstHTML = payloads[0]; const secondHTML = payloads[1]; @@ -178,7 +178,7 @@ async function getCategory(ctx) { } catch (error) { return { pageType: 'category', id: id, title: category.title, style: 'media.posterGrid', - itemAspectRatio: '16:9', page: page, hasMore: false, + itemAspectRatio: '16:9', page: page, hasMore: true, nextPage: page, selectedSortValue: sort, sort: AV591_SORTS, items: [], error: errorMessage(error) }; } diff --git a/91porny_int.js b/91porny_int.js index 7e4debb..18b046c 100644 --- a/91porny_int.js +++ b/91porny_int.js @@ -785,7 +785,7 @@ WidgetMetadata = { id: '91porny_int', title: '91Porny', description: '91Porny', - version: "0.9.6", + version: "0.9.7", requiredVersion: '0.0.1', author: "網路", site: 'https://github.com/baranwang/forward-widget', @@ -1090,7 +1090,7 @@ async function search(params) { (_params1 = params).page || (_params1.page = 1); (_params2 = params).base_url || (_params2.base_url = DEFAULT_BASE_URL); try { - const $ = await widgetAPI.getHtml(`${params.base_url}/search?keywords=${params.keyword}&page=${params.page}`); + const $ = await widgetAPI.getHtml(`${params.base_url}/search?keywords=${encodeURIComponent(params.keyword)}&page=${params.page}`); return getVideoList($, params.base_url); } catch (error) { console.error("视频列表加载失败", error); diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..552a6bf --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,35 @@ +# 2026-10-03 + +基于 `main` 的 `8e43017532ec81e3338046ba6e32dff7655bd3dd` 修复审查中 O01–O25;保留所有已发布文件路径。 + +## 加载与失败恢复 + +- MissAV 已发现完整画质时跳过关联播放器和浏览器回退;缺少地址时逐阶段解析。 +- Jable 与 591AV 首屏只请求第一组内容,其余分区懒加载。591AV 的两源页必须一起成功,任一失败后保留当前页供重试。 +- ASMRLIB 分类预览使用各自分类的内容;Manko 分类预览限并发、逐卡降级。 +- 1808 网络失败时明确标记历史推荐;详情和播放只使用实际返回的线路,不再拼造播放 URL。 +- 爱动漫只从完整的剧集区块取集数,排除历史/推荐链接;电影和单集剧分别呈现,父内容与选集 ID 独立保留。HTTP 错误页和验证页不写入内容缓存。 + +## 播放与输入兼容 + +- 麻豆两份媒体库继续探索后续线路,避免第一条 720P 隐藏可用 1080P。Pornhub 默认最高可用画质。 +- Jable 根据稳定内容 ID 刷新播放地址;XXXFollow 刷新当前条目,旧载荷只能经即时媒体验证后作为回退。MissAV Forward 的 URL 缓存限 60 秒/100 条,命中但不可用时清除并重新发现。 +- MissAV、SexBJCam、JAVGG、NO视频的播放解析共享 28 秒总时限,并限制各 HTTP/浏览器阶段及浏览器回退次数。宿主原生任务由其自身 timeout 管理,外层截止时间保证调用返回,不代表能够强制关闭原生浏览器。 +- 123AV、爱动漫、KBJ、套路SM、XXXFollow 支持对象和 JSON 字符串上下文,保留用户参数优先级及内容/集/版本 ID 的独立语义。旧 MissAV 路径兼容升级。 +- 91Porny/Pornhub 编码搜索关键词;Jable Forward 的排序和页码只添加一次。XVideos 删除原样输出会话凭证的日志。 +- 4K影视保留旧版本 ID `public-1080`,展示为自动画质;排除明显广告/预告和锁定流,多个无法确定归属的捕获流明确报错。此改动没有新增媒体元数据探测能力。 + +## 兼容维护与 Worker + +- 5 个旧下载入口同步完整 JS 副本。CI 保证主维护文件与副本一致,验证原有 39 个媒体库的 ID、参数和入口。 +- Stripchat Worker 首个有效 CDN 返回后取消其余探测;失败时仍区分全部 404/410 和暂时性故障。重试重新取得 master/pkey/pdkey;清单/JSON 的超时包含正文读取,并限制小响应大小,分片保持流式传输。 +- 增加不包含 Cookie/token/完整签名 URL 的阶段耗时日志,以及 5% traces 采样配置。 +- 新增离线回归、ZIP/索引/hash/版本一致性和 FWENC2/Forward 封装校验。 + +## 验证范围与待办 + +46 项媒体库回归、10 项 Worker 回归、原有 SexBJCam 网络恢复和 Angel Live 插件契约测试通过;普通脚本语法、39 个媒体库契约、兼容副本及已发布包一致性通过。 + +欧乐 O26 已对照一个公开网页样本(影片 24813):网页起播正常,master、变体与分片均返回 200;同一组 URL 在无 Origin、网页 Origin、现有 CDN Origin 三种设置下的范围请求全部返回 206,未复现拒绝。因此本轮保持既有请求头;仍需更多影片、跨 CDN 与 DreamBy 设备样本。浏览器网页起播不等于设备端能够回传最终媒体地址。设备上的首次起播、久置后播放、切画质及浏览器交回播放器仍需验证。 + +本次提交只更新 Worker 仓库源码和配置,未部署 Cloudflare,也未改 Stripchat 插件 ZIP 或订阅索引。 diff --git a/MissAV 3.0.js b/MissAV 3.0.js index 623d48c..060bf7c 100644 --- a/MissAV 3.0.js +++ b/MissAV 3.0.js @@ -3,7 +3,7 @@ WidgetMetadata = { title: "MissAV_ovo", author: "𝙈𝙖𝙠𝙠𝙖𝙋𝙖𝙠𝙠𝙖|CC|EL|Eric|墨白", description: "MissAV 视频聚合模块,支持其他模块聚合missav资源、支持高清海报、封面图、预告片、相似推荐、演员信息及头像", - version: "3.0", + version: "3.0.1", requiredVersion: "0.0.1", site: "https://missav.ai", modules: [ @@ -1794,7 +1794,26 @@ async function loadResource(params = {}) { const code = extractCodeFromParams(params); if (!code) return []; - let cached = VIDEO_URL_CACHE[code.toUpperCase()]; + const cacheKey = code.toUpperCase(); + let cached = VIDEO_URL_CACHE[cacheKey]; + if (cached && Date.now() - Number(cached.timestamp || 0) >= 60000) { + delete VIDEO_URL_CACHE[cacheKey]; + cached = null; + } + if (cached) { + try { + const isHLS = /\.m3u8(?:[?#]|$)/i.test(cached.url); + const response = await Widget.http.get(cached.url, { + headers: { ...HEADERS, Referer: cached.referer, ...(isHLS ? {} : { Range: 'bytes=0-0' }) }, + timeout: 5000 + }); + const status = Number(response && (response.statusCode || response.status)); + if (status < 200 || status >= 300 || !status || (isHLS && !/^\s*#EXTM3U/.test(String(response.data || '')))) throw new Error('cached media unavailable'); + } catch (_) { + delete VIDEO_URL_CACHE[cacheKey]; + cached = null; + } + } let videoUrl = cached ? cached.url : null; let currentReferer = cached?.referer || `${BASE_URL}/cn/search/${encodeURIComponent(code.trim())}`; let detailHtml = ""; @@ -1881,7 +1900,9 @@ async function loadResource(params = {}) { } if (videoUrl) { - VIDEO_URL_CACHE[code.toUpperCase()] = { url: videoUrl, timestamp: Date.now(), referer: currentReferer }; + VIDEO_URL_CACHE[cacheKey] = { url: videoUrl, timestamp: Date.now(), referer: currentReferer }; + const cacheKeys = Object.keys(VIDEO_URL_CACHE); + if (cacheKeys.length > 100) delete VIDEO_URL_CACHE[cacheKeys[0]]; } } diff --git a/README.md b/README.md new file mode 100644 index 0000000..7754c84 --- /dev/null +++ b/README.md @@ -0,0 +1,75 @@ +# DreamBy / Forward 脚本 + +本仓库同时保存 DreamBy(baiPlay)自定义媒体库、Forward 脚本和 Angel Live 插件。不同协议的版本号不能互相比较;亚洲版、国产版等不同内容分支分别维护。 + +已经导入 DreamBy 的入口继续使用原来的下载地址。更新沿用原文件路径、媒体库 ID 和原参数名;不用删除后重新导入。每个下载入口都包含完整 JavaScript,不需要远程加载器。 + +## 主维护文件与兼容入口 + +| 主维护文件 | 版本 | 保留并同步的旧文件 | +| --- | --- | --- | +| [eporner-mini-library 2.js](eporner-mini-library%202.js) | 1.0.9 | `eporner-mini-library.js` | +| [xxxfollow-mini-library 5.js](xxxfollow-mini-library%205.js) | 1.0.1 | `xxxfollow-mini-library.js` | +| [missav-mini-library-download-working 6.js](missav-mini-library-download-working%206.js) | 1.5.10 | `missav-mini-library-download-working.js`、`missav-mini-library.js`、`missav-mini-library-CloudFlare.js` | + +Eporner、XXXFollow 的两份文件原本内容相同。MissAV 的两个旧 1.0.7 实现升级为 1.5.10 兼容副本,保留 `missav-mini-library` ID、旧参数名和 `missav://detail?` 历史条目格式。 + +日常只修改左侧主维护文件,再运行: + +```bash +node tools/sync-library-aliases.cjs +``` + +GitHub Actions 会先生成兼容副本并验证;`main` 的检查通过后,如果副本有变化,机器人只提交这 5 个兼容文件。PR 要提交同步后的副本,检查会拒绝遗漏更新。主维护文件的新实现因此能继续通过所有旧下载路径获得。 + +## 独立保留的入口 + +- 麻豆亚洲版 `madou8-mini-library 5.js` 与国产版 `madou8-domestic-mini-library.js` 内容范围和 ID 不同,分别保留。 +- Jable、Pornhub、Hanime1、MissAV 的 Forward 与 DreamBy 入口分别保留。 +- Hanime1 的 `.fwd` 包和加密的 `twitter视频.js` 保持原格式;Stripchat ZIP、两个订阅索引和插件主程序保持一致。 +- `ttdm-mini-library (1).js`、`pektino-mini-library 3.js` 保持现有文件名。 + +## 2026-10-03 更新版本 + +| 文件 | 原版本 | 新版本 | +| --- | --- | --- | +| [123av-mini-library.js](123av-mini-library.js) | 1.1.0 | 1.1.1 | +| [1808-mini-library.js](1808-mini-library.js) | 1.0.5 | 1.0.6 | +| [4kvm-dreamby-mini-library.js](4kvm-dreamby-mini-library.js) | 1.10.0 | 1.10.1 | +| [591av-mini-library.js](591av-mini-library.js) | 1.3.1 | 1.3.2 | +| [91porny_int.js](91porny_int.js) | 0.9.6 | 0.9.7 | +| [MissAV 3.0.js](MissAV%203.0.js) | 3.0 | 3.0.1 | +| [asmrlib-mini-library.js](asmrlib-mini-library.js) | 1.2.0 | 1.2.1 | +| [girigirilove-mini-library.js](girigirilove-mini-library.js) | 1.0.0 | 1.0.1 | +| [jable.js](jable.js) | 1.3.0 | 1.3.1 | +| [jable.media-library.js](jable.media-library.js) | 1.0.0 | 1.0.1 | +| [javgg-mini-library.js](javgg-mini-library.js) | 1.0.0 | 1.0.1 | +| [kbjfan-mini-library.js](kbjfan-mini-library.js) | 1.0.0 | 1.0.1 | +| [madou8-domestic-mini-library.js](madou8-domestic-mini-library.js) | 1.2.1 | 1.2.2 | +| [madou8-mini-library 5.js](madou8-mini-library%205.js) | 1.1.0 | 1.1.1 | +| [manko-fun-mini-library.js](manko-fun-mini-library.js) | 1.8.0 | 1.8.1 | +| [missav-mini-library-download-working 6.js](missav-mini-library-download-working%206.js) | 1.5.9 | 1.5.10 | +| [novipnoad-mini-library.js](novipnoad-mini-library.js) | 1.0.0 | 1.0.1 | +| [pornhub.media-library.js](pornhub.media-library.js) | 1.0.0 | 1.0.1 | +| [pornhub_int.js](pornhub_int.js) | 1.1.5 | 1.1.6 | +| [sexbjcam-mini-library.js](sexbjcam-mini-library.js) | 1.1.7 | 1.1.8 | +| [taolusm-mini-library.js](taolusm-mini-library.js) | 1.0.0 | 1.0.1 | +| [xvideos_int.js](xvideos_int.js) | 0.9.6 | 0.9.7 | +| [xxxfollow-mini-library 5.js](xxxfollow-mini-library%205.js) | 1.0.0 | 1.0.1 | + +具体行为和设备验证限制见 [CHANGELOG.md](CHANGELOG.md)。 + +## 本地验收 + +```bash +node tools/sync-library-aliases.cjs --check +node tools/verify-scripts.cjs +node --test tests/adapters.test.cjs +node sexbjcam-network-recovery.test.js +node stripchat-angellive/test.js +node --test stripchat-angellive/worker/test.js +python3 tools/verify-release.py +git diff --check +``` + +检查覆盖旧媒体库 ID/参数/入口、语法、分页失败恢复、画质选择、过期地址刷新、播放总时限,以及 ZIP/索引/版本/SHA-256 和特殊封装格式。离线测试不替代 DreamBy 设备上的浏览器媒体回传和实际起播验证。 diff --git a/asmrlib-mini-library.js b/asmrlib-mini-library.js index 6491719..7537e6f 100644 --- a/asmrlib-mini-library.js +++ b/asmrlib-mini-library.js @@ -191,7 +191,7 @@ function getManifest() { id: 'asmrlib', name: 'ASMRLIB', title: 'ASMRLIB', - version: '1.2.0', + version: '1.2.1', author: 'Alan huang', logo: ASMRLIB_LOGO, icon: ASMRLIB_LOGO, @@ -203,19 +203,6 @@ function getManifest() { async function getHome() { const html = await asmrHTTP(ASMRLIB_BASE + '/', ASMRLIB_BASE + '/'); const latest = asmrParseList(html); - const categoryItems = ASMRLIB_CATEGORIES.slice(1).map(function (category, index) { - return { - id: 'asmrlib-category-' + category.id, - title: category.title, - subtitle: '浏览 ' + category.title + ' 标签', - type: 'collection', - poster: latest[index % Math.max(latest.length, 1)] ? latest[index % latest.length].poster : ASMRLIB_LOGO, - backdrop: latest[index % Math.max(latest.length, 1)] ? latest[index % latest.length].poster : ASMRLIB_LOGO, - imageHeaders: asmrImageHeaders(), - previewItems: latest.slice(index, index + 3), - action: { type: 'category', pageId: category.id, title: category.title } - }; - }); return { pageType: 'home', id: 'asmrlib-home', @@ -224,7 +211,7 @@ async function getHome() { hero: latest.slice(0, 6), sections: [ { id: 'latest', title: '最新更新', style: 'discover.standard', lazy: false, items: latest, moreAction: { type: 'category', pageId: 'latest', title: '最新更新' } }, - { id: 'categories', title: '热门标签', style: 'discover.annualListPreview', lazy: false, items: categoryItems } + { id: 'categories', title: '热门标签', style: 'discover.annualListPreview', lazy: true, loadAction: { type: 'custom', id: 'categories', sectionId: 'categories', title: '热门标签' }, items: [] } ] }; } @@ -232,8 +219,22 @@ async function getHome() { async function getHomeSection(input) { const id = asmrText(asmrPick(input, ['sectionId', 'id'], 'latest')); if (id === 'categories') { - const home = await getHome(); - return home.sections[1]; + const categories = ASMRLIB_CATEGORIES.slice(1); + const cards = []; let cursor = 0; + await Promise.all(Array.from({ length: Math.min(2, categories.length) }, async function () { + while (cursor < categories.length) { + const index = cursor++; const category = categories[index]; + try { + const page = await getCategory({ pageId: category.id, page: 1 }); + const previews = (page.items || []).slice(0, 3); const lead = previews[0]; + if (!lead || !lead.poster) continue; + cards[index] = { id: 'asmrlib-category-' + category.id, title: category.title, subtitle: '浏览 ' + category.title + ' 标签', + type: 'collection', poster: lead.poster, backdrop: lead.backdrop || lead.poster, imageHeaders: asmrImageHeaders(), previewItems: previews, + action: { type: 'category', pageId: category.id, title: category.title } }; + } catch (_) {} + } + })); + return { id: 'categories', title: '热门标签', style: 'discover.annualListPreview', lazy: false, items: cards.filter(Boolean) }; } const page = await getCategory({ pageId: id, page: 1 }); return { id: id, title: page.title, style: 'discover.standard', lazy: false, items: page.items || [] }; diff --git a/girigirilove-mini-library.js b/girigirilove-mini-library.js index d4644a6..027e8b1 100644 --- a/girigirilove-mini-library.js +++ b/girigirilove-mini-library.js @@ -9,8 +9,8 @@ const WidgetMetadata = { id: 'girigirilove-mini-library', name: '爱动漫', title: '爱动漫', - version: '1.0.0', - author: 'EL / Codex', + version: '1.0.1', + author: 'Alan huang', logo: GIRI_ICON, icon: GIRI_ICON, site: GIRI_DEFAULT_SITE, @@ -77,6 +77,7 @@ function getManifest() { } async function getHome(ctx) { + ctx = normalizeRuntimeContext(ctx); const sections = GIRI_CATEGORIES.map(function (category) { return { id: category.id, @@ -99,6 +100,7 @@ async function getHome(ctx) { } async function getHomeSection(ctx) { + ctx = normalizeRuntimeContext(ctx); const section = findCategory(ctx && (ctx.sectionId || ctx.id)) || GIRI_CATEGORIES[0]; try { const page = await fetchCategoryPage(ctx, section, 1, 'latest'); @@ -116,6 +118,7 @@ async function getHomeSection(ctx) { } async function getCategory(ctx) { + ctx = normalizeRuntimeContext(ctx); const page = positiveInt(contextValue(ctx, 'page'), 1); const pageId = stringValue(ctx && (ctx.pageId || ctx.id)) || 'latest'; const section = findCategory(pageId) || GIRI_CATEGORIES[0]; @@ -136,6 +139,7 @@ async function getCategory(ctx) { } async function getDetail(ctx) { + ctx = normalizeRuntimeContext(ctx); const payload = decodePayload(ctx && (ctx.itemId || ctx.id || ctx.sourceId)); const id = firstNonEmpty(payload && payload.id, ctx && ctx.itemId, ctx && ctx.id); if (!id) throw new Error('爱动漫详情参数无效'); @@ -163,7 +167,8 @@ async function getDetail(ctx) { sourceName: '爱动漫', action: { type: 'play', - itemId: versionId, + itemId: encodePayload({ id: id, title: detail.title, poster: detail.poster }), + episodeId: versionId, versionId: versionId, url: episodePageURL, title: episode.title @@ -176,7 +181,7 @@ async function getDetail(ctx) { pageType: 'detail', id: encodePayload({ id: id, title: detail.title, poster: detail.poster }), title: detail.title, - type: detail.episodes.length > 1 ? 'series' : 'movie', + type: detail.isSeries ? 'series' : 'movie', poster: detail.poster, backdrop: detail.poster, detailImageAspectRatio: '2:3', @@ -186,7 +191,7 @@ async function getDetail(ctx) { overview: detail.overview, rating: detail.rating, remarks: detail.remarks, - seasons: detail.episodes.length ? [{ + seasons: detail.isSeries && detail.episodes.length ? [{ id: 'season-1', title: '播放列表', seasonNumber: 1, @@ -199,7 +204,7 @@ async function getDetail(ctx) { episodeNumber: index + 1, action: { type: 'play', - itemId: episodeId, + itemId: encodePayload({ id: id, title: detail.title, poster: detail.poster }), episodeId: episodeId, versionId: episodeId, url: episodePageURL, @@ -223,7 +228,11 @@ async function getDetail(ctx) { } async function getResourceVersions(ctx) { - const payload = decodePayload(ctx && (ctx.itemId || ctx.id || ctx.versionId || ctx.episodeId)); + ctx = normalizeRuntimeContext(ctx); + const selected = firstNonEmpty(ctx && ctx.episodeId, ctx && ctx.versionId); + const selectedPayload = decodePayload(selected); + const selectedId = firstNonEmpty(selectedPayload && selectedPayload.id, selected); + const payload = decodePayload(isPlayId(selectedId) ? selected : ctx && (ctx.itemId || ctx.id)); const id = firstNonEmpty(payload && payload.id, ctx && ctx.itemId, ctx && ctx.id); if (!id) return []; if (isPlayId(id)) { @@ -250,6 +259,7 @@ async function getResourceVersions(ctx) { } async function resolvePlayback(ctx) { + ctx = normalizeRuntimeContext(ctx); const direct = firstPlayable(ctx && ctx.playUrl, ctx && ctx.videoUrl, ctx && ctx.mediaUrl, ctx && ctx.src); const resolved = direct ? { url: direct, referer: ctx && ctx.referer } : await resolveFirstPlayable(ctx); const url = resolved && resolved.url; @@ -342,6 +352,7 @@ async function play(flagOrInput, id) { } async function search(ctx) { + ctx = normalizeRuntimeContext(ctx); const query = stringValue(ctx && (ctx.query || ctx.keyword || ctx.text)); const page = positiveInt(contextValue(ctx, 'page'), 1); if (!query) return { pageType: 'search', title: '搜索结果', items: [], page: page, hasMore: false }; @@ -366,6 +377,7 @@ async function getSearch(ctx) { } async function matchResources(ctx) { + ctx = normalizeRuntimeContext(ctx); const query = stringValue(ctx && (ctx.title || ctx.name || ctx.query || ctx.keyword)); if (!query) return { results: [] }; const results = await search({ query: query, params: ctx && ctx.params, config: ctx && ctx.config }); @@ -398,7 +410,9 @@ async function fetchText(ctx, url) { if (cached) return cached; const response = await httpGet(url, { headers: requestHeaders(ctx, url) }); const text = responseText(response); - if (!text) throw new Error('空响应'); + const status = Number(response && (response.status || response.statusCode)) || 200; + if (status >= 400) throw new Error('爱动漫源站 HTTP ' + status); + if (!text || /Just a moment|cf-mitigated|Cloudflare Ray ID|