Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 38 additions & 1 deletion danmu_api/utils/bangumi-data-util.js
Original file line number Diff line number Diff line change
Expand Up @@ -773,7 +773,44 @@ export async function searchBangumiData(keyword, siteKeys) {
results.push(...additionalDubs);
}
}
return results;

return dedupeBangumiSearchResults(results, keyword);
}

// 对搜索结果精确匹配优先排序并按源去重,同源多条目保留标题精确命中检索词的一条、其余标题并入别名;tmdb 不分季且按标题消费,不参与去重
export function dedupeBangumiSearchResults(results, keyword) {
let exactTerms = [keyword];
try {
exactTerms = [...new Set([keyword, simplized(keyword), traditionalized(keyword)])];
} catch (e) {}
results.sort((a, b) => {
const aExact = a.titles.some(t => t && exactTerms.includes(t));
const bExact = b.titles.some(t => t && exactTerms.includes(t));
if (aExact && !bExact) return -1;
if (!aExact && bExact) return 1;
return 0;
});

const dedupedResults = [];
const seenSourceIds = new Map();
for (const r of results) {
if (r.matchedSiteKey === 'tmdb') {
dedupedResults.push(r);
continue;
}
const dedupeKey = `${r.matchedSiteKey}:${r.siteId}`;
const keepIdx = seenSourceIds.get(dedupeKey);
if (keepIdx === undefined) {
seenSourceIds.set(dedupeKey, dedupedResults.length);
dedupedResults.push(r);
} else {
const kept = dedupedResults[keepIdx];
for (const t of r.titles) {
if (t && !kept.titles.includes(t)) kept.titles.push(t);
}
}
}
return dedupedResults;
}

/**
Expand Down
30 changes: 29 additions & 1 deletion danmu_api/worker.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ import { previewJsContent } from './ui/js/preview.js';
import { convertToAsciiSum } from "./utils/codec-util.js";
import { convertToDanmakuJson, handleDanmusLike } from "./utils/danmu-util.js";
import { Segment, SegmentListResponse } from "./models/dandan-model.js"
import { initBangumiData, searchBangumiData, clearBangumiDataCache } from "./utils/bangumi-data-util.js";
import { initBangumiData, searchBangumiData, clearBangumiDataCache, dedupeBangumiSearchResults } from "./utils/bangumi-data-util.js";
import { generateNipaplaySignature, parseNipaplayRelatedLinks, resolveNipaplayLink, applyShiftToDanmu } from "./utils/nipaplay-util.js";

// Mock Request class for testing
Expand Down Expand Up @@ -1125,6 +1125,34 @@ test('worker.js API endpoints', async (t) => {
});
});

// 测试 Bangumi Data 本地检索结果的同源去重
await t.test('dedupeBangumiSearchResults should dedupe same-source results and skip tmdb', () => {
const makeResult = (siteKey, siteId, titles) => ({ matchedSiteKey: siteKey, siteId, titles });

// 同源多条目:保留标题精确命中检索词的一条,其余标题并入别名
const merged = dedupeBangumiSearchResults([
makeResult('anidb', '19242', ['Re:从零开始的异世界生活 第四季 丧失篇(2026)']),
makeResult('anidb', '19242', ['Re:从零开始的异世界生活 第四季 夺还篇(2026)']),
], 'Re:从零开始的异世界生活 第四季 夺还篇(2026)');
assert.equal(merged.length, 1, `Expected merged.length === 1, but got ${merged.length}`);
assert.equal(merged[0].titles[0], 'Re:从零开始的异世界生活 第四季 夺还篇(2026)');
assert.ok(merged[0].titles.includes('Re:从零开始的异世界生活 第四季 丧失篇(2026)'), 'Expected merged titles to include the secondary title');

// tmdb 不参与去重,同 id 多条均保留
const tmdbOnly = dedupeBangumiSearchResults([
makeResult('tmdb', '123', ['标题A']),
makeResult('tmdb', '123', ['标题B']),
], '检索词');
assert.equal(tmdbOnly.length, 2, `Expected tmdbOnly.length === 2, but got ${tmdbOnly.length}`);

// 不同源 id 空间重叠(同 siteId 不同 matchedSiteKey)不合并
const crossSite = dedupeBangumiSearchResults([
makeResult('anidb', '19242', ['丧失篇']),
makeResult('bangumi', '19242', ['夺还篇']),
], '检索词');
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 () => {
Expand Down
Loading