fix: 修复手动链接搜索与直连接口未剥离 @偏移 后缀导致请求失败 (#455)
* fix: 修复手动链接搜索与直连接口未正确处理 @偏移 后缀 搜索阶段取标题前剥离 @偏移 后缀:此前带 @偏移 的链接直接传入源请求(如 bilibili 返回 404)并产生脏标题,现统一用 stripLinkOffset 取洁净链接后再取标题。 直连接口 getCommentByUrl 此前完全忽略 @偏移,现与 getComment 对齐:解析 @秒数/@%百分比 偏移、用洁净链接取弹幕、取回后应用偏移;百分比偏移按弹幕最大时间缩放,逻辑与 getComment 一致。 将散落各处的 @偏移 解析统一收口到 offset-util.js 的 stripLinkOffset(消除 7 处重复内联正则),getComment / fetchMergedComments / resolveSourceAndRealId / getCommentByUrl 均复用之。 顺带修复:animeko/bahamut 此前用仅匹配 @秒数 的正则,会静默丢弃 @% 百分比偏移,现经 stripLinkOffset 统一解析后予以保留。偏移为 0(如 @0)时不再回写 @0 后缀(原会保留,属无操作偏移,无实际影响)。 * fix: 修复合并链接偏移被重复施加导致弹幕时间整体偏移 合并链接(含 $$$ 分隔的多个子链接)的弹幕在 fetchMergedComments 中已按各子链接来源分别施加偏移,而 getComment 顶层又对整段合并弹幕再次施加单链接偏移,导致所有弹幕时间被叠加偏移一次(首个子链接无偏移标记时其弹幕被整体抬走)。 改为顶层单链接偏移仅在非合并链接时施加,与既有 danmuOffsetRules 守卫(dandan-api.js:2699)一致——合并链接的偏移完全由 fetchMergedComments 按来源处理。单链接偏移行为与直连接口 getCommentByUrl 不受影响。 * fix: 修复合并链接弹幕时长仅取单视频时长而非合并时间轴长度 合并链接(含 $$$ 分隔的多个子链接)此前在 resolveMergedDuration 中对各子链接时长取最大值,导致返回的 videoDuration 为单个视频的时长(如 3266s),而非合并后时间轴的实际长度(约 6463s),前端进度条与时长显示偏短。 改为:合并链接的时长取各子链接按 @偏移 平移后时间轴末端的最大值——即 max(子链接时长 + 应用的偏移)。偏移量直接复用 applyOffset 的语义(绝对与百分比模式下均将时间轴末端平移 offset 秒),确保返回的合并时长与弹幕实际落点完全一致,且不再手算偏移公式。单链接路径保持不变,正常场景行为不受影响。 * test: 在 worker.test.js 活跃区补充 @偏移 解析与 applyOffset 端点语义用例 --------- Co-authored-by: huangxd- <649924757@qq.com>
This commit is contained in:
1 parent
b5458edd73
commit
b06d40999e
3 files changed
+117
-64
No files matched your search
@@ -11,7 +11,7 @@ import {
|
||||
} from "../utils/cache-util.js";
|
||||
import { resolveFavoriteForSearchKeyword } from "../utils/favorite-util.js";
|
||||
import { formatDanmuResponse, convertToDanmakuJson } from "../utils/danmu-util.js";
|
||||
import { resolveOffset, resolveOffsetRule, applyOffset } from "../utils/offset-util.js";
|
||||
import { resolveOffset, resolveOffsetRule, applyOffset, stripLinkOffset } from "../utils/offset-util.js";
|
||||
import { filterMappingQualifierCandidates, filterMappingTargetCandidates, resolveAutoMatchMapping } from "../utils/auto-match-mapping-util.js";
|
||||
import {
|
||||
extractEpisodeTitle, convertChineseNumber, parseFileName, createDynamicPlatformOrder, normalizeSpaces,
|
||||
@@ -178,9 +178,21 @@ async function resolveMergedDuration(url) {
|
||||
if (!url) return 0;
|
||||
|
||||
try {
|
||||
const targetUrls = url.includes(MERGE_DELIMITER) ? extractMergedUrls(url) : [url];
|
||||
const durations = await Promise.all(targetUrls.map(resolveUrlDuration));
|
||||
return durations.reduce((maxValue, currentValue) => Math.max(maxValue, currentValue || 0), 0);
|
||||
// 单链接直接返回其时长
|
||||
if (!url.includes(MERGE_DELIMITER)) {
|
||||
return await resolveUrlDuration(url);
|
||||
}
|
||||
|
||||
// 合并链接的时长取各子链接偏移后时间轴末端的最大值,而非各子链接时长的简单取大
|
||||
const linkMetas = extractMergedUrls(url).map(stripLinkOffset);
|
||||
const durations = await Promise.all(linkMetas.map((meta) => resolveUrlDuration(meta.cleanUrl)));
|
||||
// 复用 applyOffset 的偏移语义计算各子链接末端,确保返回的合并时长与弹幕实际落点完全一致
|
||||
return linkMetas.reduce((maxEnd, meta, index) => {
|
||||
const sourceDuration = durations[index];
|
||||
if (!(sourceDuration > 0)) return maxEnd;
|
||||
const end = applyOffset([{ t: sourceDuration }], meta.offset, { usePercent: meta.percent, videoDuration: sourceDuration })[0].t;
|
||||
return end > maxEnd ? end : maxEnd;
|
||||
}, 0);
|
||||
} catch (error) {
|
||||
log('warn', `[system] [duration] 获取时长失败: ${error.message}`);
|
||||
return 0;
|
||||
@@ -614,7 +626,7 @@ async function searchAnimeBody(url, preferAnimeId = null, preferSource = null, d
|
||||
// 多链接合并解析:空格分隔的多个 URL → 聚合弹幕
|
||||
const urlRegex = /^(https?:\/\/)?([a-zA-Z0-9-]+\.)+[a-zA-Z]{2,6}(:\d+)?(\/[^\s]*)?$/;
|
||||
const spaceSeparatedUrls = queryTitle.split(/\s+/).filter(u => {
|
||||
const cleanUrl = u.replace(/@%?-?\d+(?:\.\d+)?$/, '');
|
||||
const cleanUrl = stripLinkOffset(u).cleanUrl;
|
||||
return urlRegex.test(cleanUrl);
|
||||
});
|
||||
if (spaceSeparatedUrls.length >= 2) {
|
||||
@@ -636,7 +648,7 @@ async function searchAnimeBody(url, preferAnimeId = null, preferSource = null, d
|
||||
} else if (source === 'bahamut') {
|
||||
titles.push(`【bahamut】 BahaSn${singleUrl.match(/sn=(\d+)/)?.[1] || '?'}`);
|
||||
} else {
|
||||
const pt = await sourceLogContext.run(toLogSourceName(source), () => getPageTitle(singleUrl));
|
||||
const pt = await sourceLogContext.run(toLogSourceName(source), () => getPageTitle(stripLinkOffset(singleUrl).cleanUrl));
|
||||
titles.push(`【${source}】 ${pt}`);
|
||||
}
|
||||
}
|
||||
@@ -736,8 +748,8 @@ async function searchAnimeBody(url, preferAnimeId = null, preferSource = null, d
|
||||
} else if (platform === 'hongguo') {
|
||||
pageTitle = '红果短剧';
|
||||
} else {
|
||||
// 将源标识符统一映射到日志标签规范名称
|
||||
pageTitle = await sourceLogContext.run(toLogSourceName(platform), () => getPageTitle(queryTitle));
|
||||
// 将源标识符统一映射到日志标签规范名称;取标题前剥离 @偏移 后缀,避免带偏移的链接请求失败
|
||||
pageTitle = await sourceLogContext.run(toLogSourceName(platform), () => getPageTitle(stripLinkOffset(queryTitle).cleanUrl));
|
||||
}
|
||||
|
||||
const links = [{
|
||||
@@ -1710,14 +1722,14 @@ function resolveSourceAndRealId(url) {
|
||||
// Animeko: bgm.tv/bangumi.tv/chii.in/bangumi.lol/ep/xxx → animeko:xxx(@offset)
|
||||
const bgmMatch = url.match(/(?:bgm\.tv|bangumi\.tv|bangumi\.lol|chii\.in)\/ep\/(\d+)/);
|
||||
if (bgmMatch) {
|
||||
const offsetMatch = url.match(/@(-?\d+(?:\.\d+)?)$/);
|
||||
return { source: 'animeko', realId: bgmMatch[1] + (offsetMatch ? offsetMatch[0] : '') };
|
||||
const { offset, percent } = stripLinkOffset(url);
|
||||
return { source: 'animeko', realId: bgmMatch[1] + (offset !== 0 ? `@${offset}${percent ? '%' : ''}` : '') };
|
||||
}
|
||||
// Bahamut: ani.gamer.com.tw/animeVideo.php?sn=xxx → bahamut:xxx(@offset)
|
||||
const bahaMatch = url.match(/ani\.gamer\.com\.tw\/animeVideo\.php\?sn=(\d+)/);
|
||||
if (bahaMatch) {
|
||||
const offsetMatch = url.match(/@(-?\d+(?:\.\d+)?)$/);
|
||||
return { source: 'bahamut', realId: bahaMatch[1] + (offsetMatch ? offsetMatch[0] : '') };
|
||||
const { offset, percent } = stripLinkOffset(url);
|
||||
return { source: 'bahamut', realId: bahaMatch[1] + (offset !== 0 ? `@${offset}${percent ? '%' : ''}` : '') };
|
||||
}
|
||||
// 其他平台:直接传递完整 URL
|
||||
const source = detectPlatformFromUrl(url);
|
||||
@@ -2359,20 +2371,10 @@ async function fetchMergedComments(url, animeTitle, commentId) {
|
||||
let realId = part.substring(firstColonIndex + 1);
|
||||
|
||||
// 提取链接尾部偏移值(@100/@-50 秒数偏移,@%30/@%-11 百分比偏移)
|
||||
let manualOffset = 0;
|
||||
let manualOffsetPercent = false;
|
||||
const percentMatch = realId.match(/@%(-?\d+(?:\.\d+)?)$/);
|
||||
if (percentMatch) {
|
||||
manualOffset = parseFloat(percentMatch[1]);
|
||||
manualOffsetPercent = true;
|
||||
realId = realId.substring(0, realId.length - percentMatch[0].length);
|
||||
} else {
|
||||
const offsetMatch = realId.match(/@(-?\d+(?:\.\d+)?)$/);
|
||||
if (offsetMatch) {
|
||||
manualOffset = parseFloat(offsetMatch[1]);
|
||||
realId = realId.substring(0, realId.length - offsetMatch[0].length);
|
||||
}
|
||||
}
|
||||
const linkMeta = stripLinkOffset(realId);
|
||||
const manualOffset = linkMeta.offset;
|
||||
const manualOffsetPercent = linkMeta.percent;
|
||||
realId = linkMeta.cleanUrl;
|
||||
|
||||
if (sourceName !== 'hanjutv') {
|
||||
return {
|
||||
@@ -2608,22 +2610,12 @@ export async function getComment(path, queryFormat, segmentFlag, clientIp, inclu
|
||||
const durationPromise = shouldAttachDuration ? resolveMergedDuration(url) : null;
|
||||
|
||||
// 提取单链接偏移值(@秒数 / @%百分比)
|
||||
let singleUrlOffset = 0;
|
||||
let singleUrlOffsetPercent = false;
|
||||
let cleanUrl = url;
|
||||
const percentMatch = url.match(/@%(-?\d+(?:\.\d+)?)$/);
|
||||
if (percentMatch) {
|
||||
singleUrlOffset = parseFloat(percentMatch[1]);
|
||||
singleUrlOffsetPercent = true;
|
||||
cleanUrl = url.substring(0, url.length - percentMatch[0].length);
|
||||
log("info", `[system] [LogVar-API] 检测到链接百分比偏移: ${singleUrlOffset}s`);
|
||||
} else {
|
||||
const offsetMatch = url.match(/@(-?\d+(?:\.\d+)?)$/);
|
||||
if (offsetMatch) {
|
||||
singleUrlOffset = parseFloat(offsetMatch[1]);
|
||||
cleanUrl = url.substring(0, url.length - offsetMatch[0].length);
|
||||
log("info", `[system] [LogVar-API] 检测到链接偏移: ${singleUrlOffset}s`);
|
||||
}
|
||||
const linkMeta = stripLinkOffset(url);
|
||||
const singleUrlOffset = linkMeta.offset;
|
||||
const singleUrlOffsetPercent = linkMeta.percent;
|
||||
const cleanUrl = linkMeta.cleanUrl;
|
||||
if (singleUrlOffset !== 0) {
|
||||
log("info", `[system] [LogVar-API] 检测到链接${singleUrlOffsetPercent ? '百分比' : ''}偏移: ${singleUrlOffset}s`);
|
||||
}
|
||||
|
||||
if (url && url.includes(MERGE_DELIMITER)) {
|
||||
@@ -2696,8 +2688,8 @@ export async function getComment(path, queryFormat, segmentFlag, clientIp, inclu
|
||||
}
|
||||
}
|
||||
|
||||
// 单链接偏移值应用
|
||||
if (singleUrlOffset !== 0 && danmus && Array.isArray(danmus) && danmus.length > 0) {
|
||||
// 单链接偏移值应用(合并链接已在 fetchMergedComments 中按来源分别应用,此处仅处理单链接)
|
||||
if (!(url && url.includes(MERGE_DELIMITER)) && singleUrlOffset !== 0 && danmus && Array.isArray(danmus) && danmus.length > 0) {
|
||||
if (singleUrlOffsetPercent) {
|
||||
const maxTime = Math.max(...danmus.map(d => parseFloat(String(d.p).split(',')[0]) || 0), 0);
|
||||
danmus = applyOffset(danmus, singleUrlOffset, { usePercent: true, videoDuration: maxTime || 1 });
|
||||
@@ -2850,45 +2842,67 @@ export async function getCommentByUrl(videoUrl, queryFormat, segmentFlag, includ
|
||||
let danmus = [];
|
||||
const durationPromise = shouldAttachDuration ? resolveMergedDuration(url) : null;
|
||||
|
||||
// 提取单链接偏移值(@秒数 / @%百分比)
|
||||
const linkMeta = stripLinkOffset(url);
|
||||
const singleUrlOffset = linkMeta.offset;
|
||||
const singleUrlOffsetPercent = linkMeta.percent;
|
||||
const cleanUrl = linkMeta.cleanUrl;
|
||||
if (singleUrlOffset !== 0) {
|
||||
log("info", `[system] [LogVar-API] 检测到链接${singleUrlOffsetPercent ? '百分比' : ''}偏移: ${singleUrlOffset}s`);
|
||||
}
|
||||
|
||||
// 根据URL域名判断平台并获取弹幕
|
||||
if (url.includes('.qq.com')) {
|
||||
danmus = await sourceLogContext.run('tencent', () => tencentSource.getComments(url, "qq", segmentFlag));
|
||||
danmus = await sourceLogContext.run('tencent', () => tencentSource.getComments(cleanUrl, "qq", segmentFlag));
|
||||
} else if (url.includes('.iqiyi.com')) {
|
||||
danmus = await sourceLogContext.run('iqiyi', () => iqiyiSource.getComments(url, "qiyi", segmentFlag));
|
||||
danmus = await sourceLogContext.run('iqiyi', () => iqiyiSource.getComments(cleanUrl, "qiyi", segmentFlag));
|
||||
} else if (url.includes('.mgtv.com')) {
|
||||
danmus = await sourceLogContext.run('mango', () => mangoSource.getComments(url, "imgo", segmentFlag));
|
||||
danmus = await sourceLogContext.run('mango', () => mangoSource.getComments(cleanUrl, "imgo", segmentFlag));
|
||||
} else if (url.includes('.bilibili.com') || url.includes('b23.tv')) {
|
||||
// 如果是 b23.tv 短链接,先解析为完整 URL
|
||||
if (url.includes('b23.tv')) {
|
||||
url = await sourceLogContext.run('bilibili', () => bilibiliSource.resolveB23Link(url));
|
||||
let resolvedUrl = cleanUrl;
|
||||
if (resolvedUrl.includes('b23.tv')) {
|
||||
resolvedUrl = await sourceLogContext.run('bilibili', () => bilibiliSource.resolveB23Link(resolvedUrl));
|
||||
}
|
||||
danmus = await sourceLogContext.run('bilibili', () => bilibiliSource.getComments(url, "bilibili1", segmentFlag));
|
||||
danmus = await sourceLogContext.run('bilibili', () => bilibiliSource.getComments(resolvedUrl, "bilibili1", segmentFlag));
|
||||
} else if (url.includes('.youku.com')) {
|
||||
danmus = await sourceLogContext.run('youku', () => youkuSource.getComments(url, "youku", segmentFlag));
|
||||
danmus = await sourceLogContext.run('youku', () => youkuSource.getComments(cleanUrl, "youku", segmentFlag));
|
||||
} else if (url.includes('.miguvideo.com')) {
|
||||
danmus = await sourceLogContext.run('migu', () => miguSource.getComments(url, "migu", segmentFlag));
|
||||
danmus = await sourceLogContext.run('migu', () => miguSource.getComments(cleanUrl, "migu", segmentFlag));
|
||||
} else if (url.includes('.sohu.com')) {
|
||||
danmus = await sourceLogContext.run('sohu', () => sohuSource.getComments(url, "sohu", segmentFlag));
|
||||
danmus = await sourceLogContext.run('sohu', () => sohuSource.getComments(cleanUrl, "sohu", segmentFlag));
|
||||
} else if (url.includes('.le.com')) {
|
||||
danmus = await sourceLogContext.run('leshi', () => leshiSource.getComments(url, "leshi", segmentFlag));
|
||||
danmus = await sourceLogContext.run('leshi', () => leshiSource.getComments(cleanUrl, "leshi", segmentFlag));
|
||||
} else if (url.includes('.douyin.com') || url.includes('.ixigua.com')) {
|
||||
danmus = await sourceLogContext.run('xigua', () => xiguaSource.getComments(url, "xigua", segmentFlag));
|
||||
danmus = await sourceLogContext.run('xigua', () => xiguaSource.getComments(cleanUrl, "xigua", segmentFlag));
|
||||
} else if (url.includes('.mddcloud.com.cn')) {
|
||||
danmus = await sourceLogContext.run('maiduidui', () => maiduiduiSource.getComments(url, "maiduidui", segmentFlag));
|
||||
danmus = await sourceLogContext.run('maiduidui', () => maiduiduiSource.getComments(cleanUrl, "maiduidui", segmentFlag));
|
||||
} else if (url.includes('.yfsp.tv')) {
|
||||
danmus = await sourceLogContext.run('aiyifan', () => aiyifanSource.getComments(url, "aiyifan", segmentFlag));
|
||||
} else if (isHongguoPlayerUrl(url)) {
|
||||
danmus = await sourceLogContext.run('hongguo', () => hongguoSource.getComments(url, "hongguo", segmentFlag));
|
||||
danmus = await sourceLogContext.run('aiyifan', () => aiyifanSource.getComments(cleanUrl, "aiyifan", segmentFlag));
|
||||
} else if (isHongguoPlayerUrl(cleanUrl)) {
|
||||
danmus = await sourceLogContext.run('hongguo', () => hongguoSource.getComments(cleanUrl, "hongguo", segmentFlag));
|
||||
} else {
|
||||
// 如果不是已知平台,尝试第三方弹幕服务器
|
||||
const urlPattern = /^(https?:\/\/)?([\w.-]+)\.([a-z]{2,})(\/.*)?$/i;
|
||||
if (urlPattern.test(url)) {
|
||||
danmus = await sourceLogContext.run('other', () => otherSource.getComments(url, "other_server", segmentFlag));
|
||||
if (urlPattern.test(cleanUrl)) {
|
||||
danmus = await sourceLogContext.run('other', () => otherSource.getComments(cleanUrl, "other_server", segmentFlag));
|
||||
}
|
||||
}
|
||||
|
||||
log("info", `[system] [LogVar-API] Successfully fetched ${danmus.length} comments from URL`);
|
||||
|
||||
// 单链接偏移值应用
|
||||
if (singleUrlOffset !== 0 && danmus && Array.isArray(danmus) && danmus.length > 0) {
|
||||
if (singleUrlOffsetPercent) {
|
||||
const maxTime = Math.max(...danmus.map(d => parseFloat(String(d.p).split(',')[0]) || 0), 0);
|
||||
danmus = applyOffset(danmus, singleUrlOffset, { usePercent: true, videoDuration: maxTime || 1 });
|
||||
log("info", `[system] [LogVar-API] 应用链接百分比偏移 ${singleUrlOffset}s (时长=${maxTime}s)`);
|
||||
} else {
|
||||
danmus = applyOffset(danmus, singleUrlOffset);
|
||||
log("info", `[system] [LogVar-API] 应用链接偏移 ${singleUrlOffset}s`);
|
||||
}
|
||||
}
|
||||
|
||||
// 缓存弹幕结果
|
||||
if (danmus.length > 0) {
|
||||
setCommentCache(cacheKey, danmus);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { normalizeSpaces } from './common-util.js';
|
||||
|
||||
// 弹幕时间偏移独立模块
|
||||
// 职责:解析偏移规则、匹配偏移量、应用偏移到弹幕
|
||||
// 职责:解析链接 @偏移 与偏移规则、匹配偏移量、应用偏移到弹幕
|
||||
|
||||
// 来源→平台别名映射(source 名和 platform 名不一致时扩展匹配)
|
||||
const SOURCE_ALIASES = {
|
||||
@@ -263,3 +263,21 @@ export function applyOffset(danmus, offsetSeconds, options = {}) {
|
||||
return updated;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 从播放链接尾部解析 @偏移 后缀(@秒数 / @%百分比),返回剥离后的洁净链接与偏移参数
|
||||
* @param {string} rawUrl 原始链接
|
||||
* @returns {{cleanUrl: string, offset: number, percent: boolean}} 洁净链接、偏移秒数、是否百分比
|
||||
*/
|
||||
export function stripLinkOffset(rawUrl) {
|
||||
const url = String(rawUrl ?? '');
|
||||
const percentMatch = url.match(/@%(-?\d+(?:\.\d+)?)$/);
|
||||
if (percentMatch) {
|
||||
return { cleanUrl: url.substring(0, url.length - percentMatch[0].length), offset: parseFloat(percentMatch[1]), percent: true };
|
||||
}
|
||||
const offsetMatch = url.match(/@(-?\d+(?:\.\d+)?)$/);
|
||||
if (offsetMatch) {
|
||||
return { cleanUrl: url.substring(0, url.length - offsetMatch[0].length), offset: parseFloat(offsetMatch[1]), percent: false };
|
||||
}
|
||||
return { cleanUrl: url, offset: 0, percent: false };
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import test from 'node:test';
|
||||
import assert from 'node:assert';
|
||||
import { handleRequest } from './worker.js';
|
||||
import { extractTitleSeasonEpisode, getBangumi, getComment, getCommentByUrl, matchAnime, searchAnime, buildSearchAnimeUrl } from "./apis/dandan-api.js";
|
||||
import { stripLinkOffset, applyOffset } from "./utils/offset-util.js";
|
||||
import { handleFavoriteRefresh } from './apis/favorite-api.js';
|
||||
import { handleClearCache } from './apis/system-api.js';
|
||||
import { getRedisCaches, getRedisKey, pingRedis, setRedisKey, setRedisKeyWithExpiry, updateRedisCaches } from "./utils/redis-util.js";
|
||||
@@ -1168,6 +1169,28 @@ test('worker.js API endpoints', async (t) => {
|
||||
});
|
||||
});
|
||||
|
||||
await t.test('stripLinkOffset 解析 @偏移 后缀(@秒数 / @%百分比 / 无偏移 / 合并链接仅取末段)', async () => {
|
||||
assert.deepEqual(stripLinkOffset('https://x.com/v/1'), { cleanUrl: 'https://x.com/v/1', offset: 0, percent: false });
|
||||
assert.deepEqual(stripLinkOffset('https://x.com/v/1@3197'), { cleanUrl: 'https://x.com/v/1', offset: 3197, percent: false });
|
||||
assert.deepEqual(stripLinkOffset('https://x.com/v/1@%50'), { cleanUrl: 'https://x.com/v/1', offset: 50, percent: true });
|
||||
assert.deepEqual(stripLinkOffset('https://x.com/v/1@-50'), { cleanUrl: 'https://x.com/v/1', offset: -50, percent: false });
|
||||
// 合并链接仅从整条 URL 尾部取末段子链接的 @偏移
|
||||
const merged = 'https://x.com/v/A$$$https://x.com/v/B@3197';
|
||||
assert.deepEqual(stripLinkOffset(merged), { cleanUrl: 'https://x.com/v/A$$$https://x.com/v/B', offset: 3197, percent: false });
|
||||
// 容错:非字符串入参不抛错
|
||||
assert.deepEqual(stripLinkOffset(null), { cleanUrl: '', offset: 0, percent: false });
|
||||
});
|
||||
|
||||
await t.test('applyOffset 应用 @偏移 的端点语义(绝对 = 时间+偏移,百分比端点 = 最大时间+偏移)', async () => {
|
||||
const danmus = [{ p: '10,1,16777215,b' }, { p: '20,1,16777215,b' }];
|
||||
// 绝对偏移:每条弹幕时间整体平移 offset 秒
|
||||
assert.deepEqual(applyOffset(danmus, 50).map(d => d.p), ['60.00,1,16777215,b', '70.00,1,16777215,b']);
|
||||
// 百分比偏移:按时间轴缩放,scaleRatio=(maxTime+offset)/maxTime,端点 maxTime → maxTime+offset
|
||||
assert.deepEqual(applyOffset(danmus, 50, { usePercent: true, videoDuration: 20 }).map(d => d.p), ['35.00,1,16777215,b', '70.00,1,16777215,b']);
|
||||
// 合并时长端点一致性:单链接时长 3266、偏移 3197 时,合并时间轴末端为 6463
|
||||
assert.equal(applyOffset([{ t: 3266 }], 3197, { usePercent: false, videoDuration: 3266 })[0].t, 6463);
|
||||
});
|
||||
|
||||
// 测试 Bangumi Data 本地检索结果的同源去重
|
||||
await t.test('dedupeBangumiSearchResults should dedupe same-source results and skip tmdb', () => {
|
||||
const makeResult = (siteKey, siteId, titles) => ({ matchedSiteKey: siteKey, siteId, titles });
|
||||
@@ -1196,8 +1219,6 @@ test('worker.js API endpoints', async (t) => {
|
||||
assert.equal(crossSite.length, 2, `Expected crossSite.length === 2, but got ${crossSite.length}`);
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
// await t.test('GET /api/v2/comment/:id?format=json&duration=true should return segment duration and reuse comment cache', async () => {
|
||||
// Globals.init({});
|
||||
// Globals.animes = [];
|
||||
|
||||
Reference in new issue
Block a user