Fix(hanjutv): 修复韩剧TV搜索候选错配并提升结果稳定性 (#367)
* 优化韩剧TV暖身恢复与结果顺序 暖身仅在成功后记录身份,失败后允许下一次搜索重试,并保持并发请求共享同一暖身任务。 详情请求继续全部并行,完成后按候选顺序写入结果与缓存,同时保留单候选异常隔离;最小回归测试并入worker.test.js,版本升级至1.19.15。 * 修复韩剧TV搜索候选跨季错配 S5与极速版候选仅在NFKC与首尾空白规范化后的剧名完全一致时合并,删除双向模糊标题和季号叠加判断。 同名多条目使用播放模式、发布年份、集数和分类唯一消歧;仍有歧义时保持独立,最小回归测试并入worker.test.js。 * 修复韩剧TV同名候选配对与年份解析 将S5与TV同名候选改为双向唯一最佳匹配,保留无年份一对一合并并消除返回顺序导致的错配。 修正秒、毫秒、字符串时间戳和紧凑日期的年份解析,补充歧义、顺序不变与旧年份回归测试。
This commit is contained in:
1 parent
617ae3b230
commit
6ad5a2d5bb
3 files changed
+364
-31
No files matched your search
@@ -13,7 +13,7 @@ export const Globals = {
|
||||
accessedEnvVars: {},
|
||||
|
||||
// 静态常量
|
||||
VERSION: '1.19.14',
|
||||
VERSION: '1.19.15',
|
||||
MAX_LOGS: 1000, // 日志存储,最多保存 1000 行
|
||||
MAX_RECORDS: 100, // 请求记录最大数量
|
||||
|
||||
|
||||
+216
-30
@@ -39,7 +39,7 @@ export default class HanjutvSource extends BaseSource {
|
||||
this._mobileMakeHeaders = null;
|
||||
this._tvMakeHeaders = null;
|
||||
this._mobileWarmupPromise = null;
|
||||
this._mobileWarmupAttempted = false;
|
||||
this._mobileWarmedUid = null;
|
||||
}
|
||||
|
||||
getDanmuHeaders() {
|
||||
@@ -212,13 +212,180 @@ export default class HanjutvSource extends BaseSource {
|
||||
return Array.from(map.values());
|
||||
}
|
||||
|
||||
isMergeableSearchPair(leftItem, rightItem, keyword = "") {
|
||||
if (!leftItem?.name || !rightItem?.name) return false;
|
||||
if (keyword) {
|
||||
if (!titleMatches(leftItem.name, keyword) || !titleMatches(rightItem.name, keyword)) return false;
|
||||
// 双端身份键只做 Unicode 兼容归一与首尾去空白,不使用模糊标题清洗。
|
||||
normalizeSearchPairTitle(name = "") {
|
||||
return String(name || "").normalize("NFKC").trim();
|
||||
}
|
||||
|
||||
getSearchPairYear(item) {
|
||||
const rawYear = item?.publishTime ?? item?.releaseTime ?? item?.year ?? null;
|
||||
const yearText = String(rawYear ?? "").trim();
|
||||
const isReasonableYear = year => Number.isInteger(year) && year >= 1900 && year <= 2100;
|
||||
const parseDateYear = value => {
|
||||
const parsed = new Date(value);
|
||||
const year = parsed.getUTCFullYear();
|
||||
return isReasonableYear(year) ? year : null;
|
||||
};
|
||||
|
||||
if (/^(?:19|20)\d{2}$/.test(yearText)) return Number(yearText);
|
||||
|
||||
const compactDateMatch = yearText.match(/^((?:19|20)\d{2})\d{4}$/);
|
||||
if (compactDateMatch) return Number(compactDateMatch[1]);
|
||||
|
||||
if (rawYear !== null && rawYear !== "") {
|
||||
const numericValue = Number(rawYear);
|
||||
if (Number.isFinite(numericValue)) {
|
||||
if (numericValue !== 0) {
|
||||
const absoluteValue = Math.abs(numericValue);
|
||||
let timestamp = null;
|
||||
if (absoluteValue >= 10_000_000_000) {
|
||||
timestamp = numericValue;
|
||||
} else if (absoluteValue >= 100_000_000) {
|
||||
timestamp = numericValue * 1000;
|
||||
}
|
||||
|
||||
const year = timestamp === null ? null : parseDateYear(timestamp);
|
||||
if (year !== null) return year;
|
||||
}
|
||||
} else if (yearText) {
|
||||
const year = parseDateYear(yearText);
|
||||
if (year !== null) return year;
|
||||
}
|
||||
}
|
||||
|
||||
return titleMatches(leftItem.name, rightItem.name) && titleMatches(rightItem.name, leftItem.name);
|
||||
const memoMatch = String(item?.searchMemo || "").match(/(?:19|20)\d{2}/);
|
||||
return memoMatch ? Number(memoMatch[0]) : null;
|
||||
}
|
||||
|
||||
getSearchPairEpisodeCount(item) {
|
||||
const value = Number(item?.lastSerialNo ?? item?.totalEpisode ?? item?.episodeCount);
|
||||
return Number.isFinite(value) && value > 0 ? value : null;
|
||||
}
|
||||
|
||||
getSearchPairMetadata(item) {
|
||||
const normalizeValue = value => {
|
||||
if (value === undefined || value === null) return null;
|
||||
const normalized = String(value).trim();
|
||||
return normalized || null;
|
||||
};
|
||||
return {
|
||||
playMode: normalizeValue(item?.playMode),
|
||||
year: normalizeValue(this.getSearchPairYear(item)),
|
||||
episodeCount: normalizeValue(this.getSearchPairEpisodeCount(item)),
|
||||
category: normalizeValue(item?.category),
|
||||
};
|
||||
}
|
||||
|
||||
// titleMatches 只负责关键词相关性;S5↔TV 是否为同一实体必须满足精确标题与强元数据约束。
|
||||
isMergeableSearchPair(leftItem, rightItem) {
|
||||
const leftTitle = this.normalizeSearchPairTitle(leftItem?.name);
|
||||
const rightTitle = this.normalizeSearchPairTitle(rightItem?.name);
|
||||
if (!leftTitle || !rightTitle || leftTitle !== rightTitle) return false;
|
||||
|
||||
const leftMeta = this.getSearchPairMetadata(leftItem);
|
||||
const rightMeta = this.getSearchPairMetadata(rightItem);
|
||||
for (const field of ["playMode", "year", "category"]) {
|
||||
if (leftMeta[field] !== null && rightMeta[field] !== null && leftMeta[field] !== rightMeta[field]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
getSearchPairMatchScore(leftItem, rightItem) {
|
||||
if (!this.isMergeableSearchPair(leftItem, rightItem)) return null;
|
||||
const leftMeta = this.getSearchPairMetadata(leftItem);
|
||||
const rightMeta = this.getSearchPairMetadata(rightItem);
|
||||
const weights = { year: 8, playMode: 4, category: 2, episodeCount: 1 };
|
||||
let score = 0;
|
||||
|
||||
for (const [field, weight] of Object.entries(weights)) {
|
||||
if (leftMeta[field] !== null && rightMeta[field] !== null && leftMeta[field] === rightMeta[field]) {
|
||||
score += weight;
|
||||
}
|
||||
}
|
||||
|
||||
return score;
|
||||
}
|
||||
|
||||
selectUniqueBestSearchCandidate(item, candidates = []) {
|
||||
const scored = candidates
|
||||
.map(candidate => ({ candidate, score: this.getSearchPairMatchScore(item, candidate) }))
|
||||
.filter(entry => entry.score !== null);
|
||||
if (scored.length === 0) return null;
|
||||
|
||||
const bestScore = Math.max(...scored.map(entry => entry.score));
|
||||
const best = scored.filter(entry => entry.score === bestScore);
|
||||
return best.length === 1 ? best[0].candidate : null;
|
||||
}
|
||||
|
||||
pairSearchCandidateGroup(s5Items = [], tvItems = []) {
|
||||
const remainingS5 = new Map(s5Items.map(item => [String(item.sid), item]));
|
||||
const remainingTv = new Map(tvItems.map(item => [String(item.sid), item]));
|
||||
const pairedTvByS5Sid = new Map();
|
||||
|
||||
while (remainingS5.size > 0 && remainingTv.size > 0) {
|
||||
const currentS5 = Array.from(remainingS5.values());
|
||||
const currentTv = Array.from(remainingTv.values());
|
||||
const bestTvByS5Sid = new Map();
|
||||
const bestS5ByTvSid = new Map();
|
||||
|
||||
for (const s5Item of currentS5) {
|
||||
const bestTv = this.selectUniqueBestSearchCandidate(s5Item, currentTv);
|
||||
if (bestTv) bestTvByS5Sid.set(String(s5Item.sid), bestTv);
|
||||
}
|
||||
for (const tvItem of currentTv) {
|
||||
const bestS5 = this.selectUniqueBestSearchCandidate(tvItem, currentS5);
|
||||
if (bestS5) bestS5ByTvSid.set(String(tvItem.sid), bestS5);
|
||||
}
|
||||
|
||||
const mutualPairs = [];
|
||||
for (const [s5Sid, tvItem] of bestTvByS5Sid) {
|
||||
const tvSid = String(tvItem.sid);
|
||||
const bestS5 = bestS5ByTvSid.get(tvSid);
|
||||
if (bestS5 && String(bestS5.sid) === s5Sid) {
|
||||
mutualPairs.push({ s5Sid, tvSid, tvItem });
|
||||
}
|
||||
}
|
||||
|
||||
if (mutualPairs.length === 0) break;
|
||||
for (const pair of mutualPairs) {
|
||||
pairedTvByS5Sid.set(pair.s5Sid, pair.tvItem);
|
||||
remainingS5.delete(pair.s5Sid);
|
||||
remainingTv.delete(pair.tvSid);
|
||||
}
|
||||
}
|
||||
|
||||
return pairedTvByS5Sid;
|
||||
}
|
||||
|
||||
pairSearchCandidates(s5Items = [], tvItems = []) {
|
||||
const groupByTitle = items => {
|
||||
const groups = new Map();
|
||||
for (const item of items) {
|
||||
const title = this.normalizeSearchPairTitle(item?.name);
|
||||
if (!title) continue;
|
||||
if (!groups.has(title)) groups.set(title, []);
|
||||
groups.get(title).push(item);
|
||||
}
|
||||
return groups;
|
||||
};
|
||||
|
||||
const s5Groups = groupByTitle(s5Items);
|
||||
const tvGroups = groupByTitle(tvItems);
|
||||
const pairedTvByS5Sid = new Map();
|
||||
|
||||
for (const [title, s5Group] of s5Groups) {
|
||||
const tvGroup = tvGroups.get(title);
|
||||
if (!tvGroup) continue;
|
||||
const groupPairs = this.pairSearchCandidateGroup(s5Group, tvGroup);
|
||||
for (const [s5Sid, tvItem] of groupPairs) {
|
||||
pairedTvByS5Sid.set(s5Sid, tvItem);
|
||||
}
|
||||
}
|
||||
|
||||
return pairedTvByS5Sid;
|
||||
}
|
||||
|
||||
buildSearchCandidate(item, variant, linkedSid = "") {
|
||||
@@ -256,12 +423,12 @@ export default class HanjutvSource extends BaseSource {
|
||||
const hasMatched = s5.matched.length + tv.matched.length > 0;
|
||||
|
||||
const resultList = [];
|
||||
const usedTvSids = new Set();
|
||||
const pairedTvByS5Sid = this.pairSearchCandidates(s5.matched, tv.matched);
|
||||
const usedTvSids = new Set(Array.from(pairedTvByS5Sid.values(), item => String(item.sid)));
|
||||
|
||||
for (const item of s5.matched) {
|
||||
const pairedTv = tv.matched.find(candidate => !usedTvSids.has(String(candidate.sid)) && this.isMergeableSearchPair(item, candidate, keyword));
|
||||
const pairedTv = pairedTvByS5Sid.get(String(item.sid));
|
||||
if (pairedTv) {
|
||||
usedTvSids.add(String(pairedTv.sid));
|
||||
resultList.push(this.buildSearchCandidate(item, HANJUTV_VARIANTS.MERGED, pairedTv.sid));
|
||||
} else {
|
||||
resultList.push(this.buildSearchCandidate(item, HANJUTV_VARIANTS.HXQ));
|
||||
@@ -332,24 +499,34 @@ export default class HanjutvSource extends BaseSource {
|
||||
async warmupMobileIdentity(headers) {
|
||||
try {
|
||||
await httpGet(`${this.appHost}/api/common/configs`, { headers, timeout: 8000, retries: 0 });
|
||||
return true;
|
||||
} catch (_) {
|
||||
// 暖身失败不阻断搜索
|
||||
// 暖身失败不阻断搜索,下一次搜索会再次尝试。
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async ensureMobileIdentityWarmed() {
|
||||
if (this._mobileWarmupAttempted) {
|
||||
await this._mobileWarmupPromise;
|
||||
return;
|
||||
}
|
||||
if (this._mobileWarmedUid) return true;
|
||||
if (this._mobileWarmupPromise) return this._mobileWarmupPromise;
|
||||
|
||||
this._mobileWarmupAttempted = true;
|
||||
this._mobileWarmupPromise = (async () => {
|
||||
const warmupPromise = (async () => {
|
||||
const headerInfo = await this.buildMobileHeaders();
|
||||
await this.warmupMobileIdentity(headerInfo.headers);
|
||||
if (this._mobileWarmedUid === headerInfo.uid) return true;
|
||||
|
||||
const warmed = await this.warmupMobileIdentity(headerInfo.headers);
|
||||
if (warmed) this._mobileWarmedUid = headerInfo.uid;
|
||||
return warmed;
|
||||
})();
|
||||
|
||||
await this._mobileWarmupPromise;
|
||||
this._mobileWarmupPromise = warmupPromise;
|
||||
try {
|
||||
return await warmupPromise;
|
||||
} finally {
|
||||
if (this._mobileWarmupPromise === warmupPromise) {
|
||||
this._mobileWarmupPromise = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async searchWithS5Api(keyword) {
|
||||
@@ -698,21 +875,30 @@ export default class HanjutvSource extends BaseSource {
|
||||
}
|
||||
}
|
||||
|
||||
await Promise.all(
|
||||
const payloads = await Promise.all(
|
||||
filteredAnimes.map(async (anime) => {
|
||||
try {
|
||||
const payload = await this.buildAnimePayload(anime);
|
||||
if (!payload || !payload.summary || !Array.isArray(payload.links) || payload.links.length === 0) return;
|
||||
|
||||
tmpAnimes.push(payload.summary);
|
||||
addAnime({ ...payload.summary, links: payload.links }, detailStore);
|
||||
if (globals.animes.length > globals.MAX_ANIMES) removeEarliestAnime();
|
||||
} catch (error) {
|
||||
log("error", `[Hanjutv] Error processing anime: ${error.message}`);
|
||||
}
|
||||
})
|
||||
try {
|
||||
const payload = await this.buildAnimePayload(anime);
|
||||
if (!payload || !payload.summary || !Array.isArray(payload.links) || payload.links.length === 0) return null;
|
||||
return payload;
|
||||
} catch (error) {
|
||||
log("error", `[Hanjutv] Error processing anime: ${error.message}`);
|
||||
return null;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
||||
for (const payload of payloads) {
|
||||
if (!payload) continue;
|
||||
try {
|
||||
tmpAnimes.push(payload.summary);
|
||||
addAnime({ ...payload.summary, links: payload.links }, detailStore);
|
||||
if (globals.animes.length > globals.MAX_ANIMES) removeEarliestAnime();
|
||||
} catch (error) {
|
||||
log("error", `[Hanjutv] Error processing anime: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
this.sortAndPushAnimesByYear(tmpAnimes, curAnimes);
|
||||
return tmpAnimes;
|
||||
}
|
||||
|
||||
@@ -1148,6 +1148,153 @@ test('worker.js API endpoints', async (t) => {
|
||||
// assert(res.length > 2, `Expected res.length > 2, but got ${res.length}`);
|
||||
// });
|
||||
|
||||
await t.test('Hanjutv warmup should retry after failure and share concurrent promise', async () => {
|
||||
const source = new HanjutvSource();
|
||||
let attempts = 0;
|
||||
let finishFirst;
|
||||
source.buildMobileHeaders = async () => ({ uid: 'stable-uid', headers: {} });
|
||||
source.warmupMobileIdentity = async () => {
|
||||
attempts++;
|
||||
if (attempts === 1) return new Promise(resolve => { finishFirst = resolve; });
|
||||
return true;
|
||||
};
|
||||
|
||||
const concurrent = [source.ensureMobileIdentityWarmed(), source.ensureMobileIdentityWarmed()];
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.equal(attempts, 1);
|
||||
finishFirst(false);
|
||||
await Promise.all(concurrent);
|
||||
await source.ensureMobileIdentityWarmed();
|
||||
await source.ensureMobileIdentityWarmed();
|
||||
assert.equal(attempts, 2);
|
||||
});
|
||||
|
||||
await t.test('Hanjutv details should stay fully parallel and preserve candidate order', async () => {
|
||||
const source = new HanjutvSource();
|
||||
const candidates = Array.from({ length: 6 }, (_, index) => ({ sid: `sid-${index}`, name: `顺序测试剧${index}` }));
|
||||
const resolvers = new Map();
|
||||
const started = [];
|
||||
const previous = { animes: Globals.animes, episodeIds: Globals.episodeIds, episodeNum: Globals.episodeNum };
|
||||
Globals.animes = [];
|
||||
Globals.episodeIds = [];
|
||||
Globals.episodeNum = 10001;
|
||||
source.buildAnimePayload = anime => new Promise(resolve => {
|
||||
started.push(anime.sid);
|
||||
resolvers.set(anime.sid, resolve);
|
||||
});
|
||||
source.sortAndPushAnimesByYear = (items, target) => target.push(...items);
|
||||
|
||||
try {
|
||||
const current = [];
|
||||
const task = source.handleAnimes(candidates, '顺序测试剧', current, new Map());
|
||||
await new Promise(resolve => setImmediate(resolve));
|
||||
assert.deepEqual(started, candidates.map(item => item.sid));
|
||||
[...candidates].reverse().forEach(anime => {
|
||||
const index = candidates.indexOf(anime);
|
||||
resolvers.get(anime.sid)({
|
||||
summary: { animeId: 900000 + index, bangumiId: String(900000 + index), animeTitle: anime.name, type: '韩剧', typeDescription: '韩剧', imageUrl: '', startDate: '2025-01-01T00:00:00Z', episodeCount: 1, rating: 0, isFavorited: true, source: 'hanjutv' },
|
||||
links: [{ name: '第1集', url: `hxq:${anime.sid}`, title: '【hanjutv】 第1集' }],
|
||||
});
|
||||
});
|
||||
const expected = candidates.map(item => item.name);
|
||||
assert.deepEqual((await task).map(item => item.animeTitle), expected);
|
||||
assert.deepEqual(current.map(item => item.animeTitle), expected);
|
||||
assert.deepEqual(Globals.animes.map(item => item.animeTitle), expected);
|
||||
} finally {
|
||||
Globals.animes = previous.animes;
|
||||
Globals.episodeIds = previous.episodeIds;
|
||||
Globals.episodeNum = previous.episodeNum;
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('Hanjutv should merge only exact titles and disambiguate duplicate names', async () => {
|
||||
const source = new HanjutvSource();
|
||||
const getMergedPairs = (keyword, s5Items, tvItems) => source
|
||||
.mergeSearchCandidates(keyword, s5Items, tvItems)
|
||||
.resultList
|
||||
.filter(item => item._variant === 'merged')
|
||||
.map(item => [item.sid, item.tvSid])
|
||||
.sort((left, right) => left[0].localeCompare(right[0]));
|
||||
|
||||
const taxi = source.mergeSearchCandidates('模范出租车', [
|
||||
{ sid: 's3', name: '模范出租车3' },
|
||||
{ sid: 's2', name: '模范出租车2' },
|
||||
], [
|
||||
{ sid: 't2', name: '模范出租车2' },
|
||||
{ sid: 't3', name: '模范出租车3' },
|
||||
]).resultList.filter(item => item._variant === 'merged');
|
||||
assert.deepEqual(taxi.map(item => [item.name, item.tvSid]), [
|
||||
['模范出租车3', 't3'],
|
||||
['模范出租车2', 't2'],
|
||||
]);
|
||||
|
||||
const duplicate = source.mergeSearchCandidates('配对游戏', [
|
||||
{ sid: 's-new', name: '配对游戏', playMode: 100, publishTime: '2025-01-01', lastSerialNo: 6 },
|
||||
{ sid: 's-old', name: '配对游戏', playMode: 101, publishTime: '2024-01-01', lastSerialNo: 63 },
|
||||
], [
|
||||
{ sid: 't-old', name: '配对游戏', playMode: 101, publishTime: '2024-01-01', lastSerialNo: 63 },
|
||||
{ sid: 't-new', name: '配对游戏', playMode: 100, publishTime: '2025-01-01', lastSerialNo: 6 },
|
||||
]).resultList.filter(item => item._variant === 'merged');
|
||||
assert.deepEqual(duplicate.map(item => item.tvSid), ['t-new', 't-old']);
|
||||
|
||||
const partialS5 = [
|
||||
{ sid: 's-unknown', name: '同名剧', playMode: 100, category: 1 },
|
||||
{ sid: 's-2025', name: '同名剧', playMode: 100, publishTime: '2025-01-01', category: 1 },
|
||||
];
|
||||
const datedTv = [
|
||||
{ sid: 't-2025', name: '同名剧', playMode: 100, publishTime: '2025-01-01', category: 1 },
|
||||
];
|
||||
for (const s5Order of [partialS5, [...partialS5].reverse()]) {
|
||||
assert.deepEqual(getMergedPairs('同名剧', s5Order, datedTv), [['s-2025', 't-2025']]);
|
||||
}
|
||||
|
||||
const ambiguousS5 = [
|
||||
{ sid: 's-a', name: '歧义剧', playMode: 100, category: 1 },
|
||||
{ sid: 's-b', name: '歧义剧', playMode: 100, category: 1 },
|
||||
];
|
||||
const ambiguousTv = [{ sid: 't-only', name: '歧义剧', playMode: 100, category: 1 }];
|
||||
assert.deepEqual(getMergedPairs('歧义剧', ambiguousS5, ambiguousTv), []);
|
||||
assert.deepEqual(getMergedPairs('歧义剧', [...ambiguousS5].reverse(), ambiguousTv), []);
|
||||
|
||||
assert.deepEqual(getMergedPairs('待播剧', [
|
||||
{ sid: 's-upcoming', name: '待播剧', playMode: 100, category: 1 },
|
||||
], [
|
||||
{ sid: 't-upcoming', name: '待播剧', playMode: 100, category: 1 },
|
||||
]), [['s-upcoming', 't-upcoming']]);
|
||||
|
||||
const eliminationS5 = [
|
||||
{ sid: 's-known', name: '排除剧', playMode: 100, publishTime: '2025-01-01', category: 1 },
|
||||
{ sid: 's-left', name: '排除剧', playMode: 100, category: 1 },
|
||||
];
|
||||
const eliminationTv = [
|
||||
{ sid: 't-left', name: '排除剧', playMode: 100, category: 1 },
|
||||
{ sid: 't-known', name: '排除剧', playMode: 100, publishTime: '2025-01-01', category: 1 },
|
||||
];
|
||||
const expectedEliminationPairs = [['s-known', 't-known'], ['s-left', 't-left']];
|
||||
for (const s5Order of [eliminationS5, [...eliminationS5].reverse()]) {
|
||||
for (const tvOrder of [eliminationTv, [...eliminationTv].reverse()]) {
|
||||
assert.deepEqual(getMergedPairs('排除剧', s5Order, tvOrder), expectedEliminationPairs);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await t.test('Hanjutv should parse search-pair years without confusing seconds and milliseconds', () => {
|
||||
const source = new HanjutvSource();
|
||||
assert.equal(source.getSearchPairYear({ publishTime: 888768000000 }), 1998);
|
||||
assert.equal(source.getSearchPairYear({ publishTime: '956678400000' }), 2000);
|
||||
assert.equal(source.getSearchPairYear({ publishTime: 1735689600 }), 2025);
|
||||
assert.equal(source.getSearchPairYear({ publishTime: '20250101' }), 2025);
|
||||
assert.equal(source.getSearchPairYear({ releaseTime: '2025-07-11T00:00:00Z' }), 2025);
|
||||
assert.equal(source.getSearchPairYear({ publishTime: 0, searchMemo: '1998·韩剧·敬请期待' }), 1998);
|
||||
assert.equal(source.getSearchPairYear({ publishTime: 'not-a-date' }), null);
|
||||
assert.equal(source.getSearchPairYear({ publishTime: 253402300800000 }), null);
|
||||
|
||||
assert.equal(source.isMergeableSearchPair(
|
||||
{ name: '千禧剧', publishTime: 974788882000 },
|
||||
{ name: '千禧剧', publishTime: 974820151000 },
|
||||
), true);
|
||||
});
|
||||
|
||||
// await t.test('GET hanjutv search', async () => {
|
||||
// const res = await hanjutvSource.search("犯罪现场Zero");
|
||||
// assert(res.length > 0, `Expected res.length > 0, but got ${res.length}`);
|
||||
|
||||
Reference in new issue
Block a user