fix(trpc): bind ranked top candidate and cover low-confidence fetch
This commit is contained in:
parent
188cf79270
commit
714f51a1a6
|
|
@ -212,6 +212,30 @@ describe("danmakuService.fetch", () => {
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("多候选低置信:不自动绑,提示手选", async () => {
|
||||||
|
vi.mocked(mediaItemDao.getByIdForUser).mockResolvedValue(mediaItem());
|
||||||
|
vi.mocked(danmakuDocsDao.get).mockResolvedValue(null);
|
||||||
|
vi.mocked(hasOpenCredentials).mockReturnValue(true);
|
||||||
|
vi.mocked(danmakuCacheDao.getValid).mockResolvedValue(null);
|
||||||
|
// 两条同分(score 1.0),lead 0 < 0.15 → highConfidence false
|
||||||
|
vi.mocked(openMatch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
errorMessage: null,
|
||||||
|
isMatched: true,
|
||||||
|
candidates: [
|
||||||
|
{ episodeId: 11, animeId: 1, animeTitle: "T", episodeTitle: "E", imageUrl: null },
|
||||||
|
{ episodeId: 22, animeId: 2, animeTitle: "T2", episodeTitle: "E2", imageUrl: null },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
const r = await danmakuService.fetch("u1", "m1");
|
||||||
|
expect(r.source).toBe("none");
|
||||||
|
expect(r.xml).toBe("");
|
||||||
|
expect(r.message).toContain("手选");
|
||||||
|
expect(mediaItemDao.setDanmakuMatch).not.toHaveBeenCalled();
|
||||||
|
expect(openCommentXml).not.toHaveBeenCalled();
|
||||||
|
expect(danmakuCacheDao.upsert).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("danmakuService.selectMatch", () => {
|
describe("danmakuService.selectMatch", () => {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
import { describe, expect, it } from "vitest";
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||||
|
import { openMatch, hasOpenCredentials } from "@app/danmaku";
|
||||||
|
import { mediaItemDao } from "@app/dao";
|
||||||
|
import type { MediaItem } from "@app/models";
|
||||||
import {
|
import {
|
||||||
AUTO_BIND_THRESHOLD,
|
AUTO_BIND_THRESHOLD,
|
||||||
buildSearchQuery,
|
buildSearchQuery,
|
||||||
|
|
@ -6,6 +9,30 @@ import {
|
||||||
pickDandanEpisode,
|
pickDandanEpisode,
|
||||||
scoreHit,
|
scoreHit,
|
||||||
} from "./scrape.service.js";
|
} from "./scrape.service.js";
|
||||||
|
import { scrapeService } from "./scrape.service.js";
|
||||||
|
|
||||||
|
vi.mock("@app/dao", () => ({
|
||||||
|
mediaItemDao: {
|
||||||
|
getByIdForUser: vi.fn(),
|
||||||
|
updateScrape: vi.fn(),
|
||||||
|
listByPathPrefix: vi.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@app/danmaku", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal<typeof import("@app/danmaku")>();
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
openMatch: vi.fn(),
|
||||||
|
openCommentXml: vi.fn(),
|
||||||
|
hasOpenCredentials: vi.fn(),
|
||||||
|
openFetchJson: vi.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
vi.mock("./danmaku.service.js", () => ({
|
||||||
|
remoteFileHash: vi.fn().mockResolvedValue("hash"),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("parseEpisodeFromFilename", () => {
|
describe("parseEpisodeFromFilename", () => {
|
||||||
it("S01E02", () => {
|
it("S01E02", () => {
|
||||||
|
|
@ -89,3 +116,57 @@ describe("scoreHit", () => {
|
||||||
expect(s).toBeLessThan(AUTO_BIND_THRESHOLD);
|
expect(s).toBeLessThan(AUTO_BIND_THRESHOLD);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function mediaItem(over: Partial<MediaItem> = {}): MediaItem {
|
||||||
|
return {
|
||||||
|
id: "m1",
|
||||||
|
userId: "u1",
|
||||||
|
mountId: null,
|
||||||
|
path: "/anime/Show - 01.mkv",
|
||||||
|
rawName: "Show - 01.mkv",
|
||||||
|
title: "Show",
|
||||||
|
size: 1024,
|
||||||
|
mime: null,
|
||||||
|
bangumiId: null,
|
||||||
|
epNumber: 1,
|
||||||
|
scrapeStatus: "pending",
|
||||||
|
scrapedAt: null,
|
||||||
|
posterUrl: null,
|
||||||
|
dandanplayEpisodeId: null,
|
||||||
|
matchedHash: null,
|
||||||
|
danmakuMatchSource: "none",
|
||||||
|
scannedAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
...over,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("scrapeService.matchDandanplay 绑定一致性", () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.mocked(hasOpenCredentials).mockReturnValue(true);
|
||||||
|
vi.mocked(openMatch).mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
errorMessage: null,
|
||||||
|
isMatched: true,
|
||||||
|
candidates: [
|
||||||
|
// score 0:无 animeTitle/episodeTitle → rankCandidates 会排到末尾
|
||||||
|
{ episodeId: 11, animeId: 1, animeTitle: null, episodeTitle: null, imageUrl: null },
|
||||||
|
// score 1.0 → ranked 第一名
|
||||||
|
{ episodeId: 22, animeId: 2, animeTitle: "T", episodeTitle: "E", imageUrl: null },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("绑定 ranked.candidates[0],且与返回 candidates[0] 一致", async () => {
|
||||||
|
vi.mocked(mediaItemDao.getByIdForUser).mockResolvedValue(mediaItem());
|
||||||
|
const r = await scrapeService.matchDandanplay("u1", "m1");
|
||||||
|
expect(r.matched).toBe(true);
|
||||||
|
expect(r.candidates[0]?.episodeId).toBe(22);
|
||||||
|
expect(mediaItemDao.updateScrape).toHaveBeenCalledWith(
|
||||||
|
"m1",
|
||||||
|
"u1",
|
||||||
|
expect.objectContaining({ dandanplayEpisodeId: 22 }),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -234,7 +234,7 @@ export const scrapeService = {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const ranked = rankCandidates(outcome.candidates);
|
const ranked = rankCandidates(outcome.candidates);
|
||||||
const best = outcome.candidates[0];
|
const best = ranked.candidates[0];
|
||||||
if (best) {
|
if (best) {
|
||||||
await mediaItemDao.updateScrape(mediaItemId, userId, {
|
await mediaItemDao.updateScrape(mediaItemId, userId, {
|
||||||
dandanplayEpisodeId: best.episodeId,
|
dandanplayEpisodeId: best.episodeId,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue