对照 MP TorrentHelper.match_torrent:已识别到同一本地作品(或同一 TMDB 身份)的属于、识别到别的作品的不属于; 未识别的先排除音乐 / 游戏 / 软件 / 书分类,再核对类型、年份(剧集按各季首播年份 ±1)、季集范围(TMDB 季列表, 单季集号可按全部季总集数的绝对集号),最后按解析出的中 / 英文名、标题非英文词、副标题非英文词比对作品名、原名、别名。 不属于的不参与筛选与分面,响应里单独给出(unmatched / unmatchedTotal,只在第一页,最多 200 条), 页面在列表最后折叠为「另有 N 条可能不是这部作品」,展开后仍可下载。关键词搜索不做这一步。 用「药屋少女的呢喃」109 条真实结果做样本:只有《迷宫饭》片尾曲专辑和标题写成 S01-S24 的一条被判为不属于。 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
200 lines
7.6 KiB
TypeScript
200 lines
7.6 KiB
TypeScript
import { readFileSync } from "node:fs";
|
|
import { fileURLToPath } from "node:url";
|
|
import type { MediaServerInventory, SearchRelease, WorkInventory } from "@umt/contracts";
|
|
import { createElement } from "react";
|
|
import { renderToStaticMarkup } from "react-dom/server";
|
|
import { MemoryRouter } from "react-router";
|
|
import { describe, expect, it } from "vitest";
|
|
import { parseWorkRef, type ReleaseFilterState, toApiFilters, UnmatchedReleases } from "./release-results.tsx";
|
|
import { resolvePick } from "./site-picker-dialog.tsx";
|
|
import { inventoryBadge, mediaServerSummary, releaseSearchPath, slotLine, workHref, workRef } from "./work-card.tsx";
|
|
|
|
// SEARCH-2:作品 → 详情 / 搜资源的地址,站点选择解析,资源查询参数,订阅入口只在作品详情页。
|
|
|
|
const inv = (p: Partial<WorkInventory>): WorkInventory => ({
|
|
state: "not_collected",
|
|
subscribed: false,
|
|
versionCount: 0,
|
|
downloadingCount: 0,
|
|
slotTotal: 0,
|
|
slotSatisfied: 0,
|
|
missing: [],
|
|
...p,
|
|
});
|
|
const ID = "11111111-1111-4111-8111-111111111111";
|
|
|
|
describe("作品地址", () => {
|
|
it("本地作品进媒体库详情;仅 TMDB 的进 TMDB 详情", () => {
|
|
expect(workHref({ localEntityId: ID, tmdb: { entityType: "movie", externalId: "1" } })).toBe(
|
|
`/console/library/${ID}`,
|
|
);
|
|
expect(workHref({ localEntityId: null, tmdb: { entityType: "tv", externalId: "1399" } })).toBe(
|
|
"/console/media/tmdb/tv/1399",
|
|
);
|
|
});
|
|
|
|
it("搜资源引用与第二阶段地址可以往返", () => {
|
|
const ref = workRef({ localEntityId: null, tmdb: { entityType: "movie", externalId: "157336" } });
|
|
expect(ref).toBe("tmdb:movie:157336");
|
|
expect(parseWorkRef(ref!)).toEqual({ tmdb: "movie:157336" });
|
|
expect(parseWorkRef(`local:${ID}`)).toEqual({ entityId: ID });
|
|
expect(parseWorkRef("q:Dune 2024")).toEqual({ q: "Dune 2024" });
|
|
expect(parseWorkRef("nope")).toBeNull();
|
|
const url = new URL(releaseSearchPath({ title: "星际穿越", ref: ref! }, ["a", "b"]), "http://x");
|
|
expect(url.pathname).toBe("/console/search");
|
|
expect(Object.fromEntries(url.searchParams)).toEqual({ q: "星际穿越", work: ref, t: "星际穿越", in: "a,b" });
|
|
});
|
|
});
|
|
|
|
describe("站点选择", () => {
|
|
const sites = [
|
|
{ id: "a", enabled: true, searchDefault: true },
|
|
{ id: "b", enabled: true, searchDefault: false },
|
|
{ id: "c", enabled: false, searchDefault: true },
|
|
];
|
|
it("默认 = 启用且默认参与;全部 = 全部启用;指定 = 选中里仍启用的", () => {
|
|
expect(resolvePick({ mode: "default", siteIds: [] }, sites)).toEqual(["a"]);
|
|
expect(resolvePick({ mode: "all", siteIds: [] }, sites)).toEqual(["a", "b"]);
|
|
expect(resolvePick({ mode: "custom", siteIds: ["b", "c"] }, sites)).toEqual(["b"]);
|
|
});
|
|
|
|
it("资源查询:选中的站点进 sites,站点分面只进 siteFilter;按作品搜不带 q", () => {
|
|
const f: ReleaseFilterState = {
|
|
sites: "b",
|
|
resolution: "",
|
|
source: "",
|
|
minGb: "",
|
|
maxGb: "",
|
|
free: "",
|
|
hr: "",
|
|
sort: "",
|
|
order: "",
|
|
};
|
|
expect(toApiFilters({ tmdb: "movie:1" }, f, "a,b")).toMatchObject({
|
|
tmdb: "movie:1",
|
|
sites: "a,b",
|
|
siteFilter: "b",
|
|
});
|
|
expect(toApiFilters({ tmdb: "movie:1" }, { ...f, sites: "" }, "")).not.toHaveProperty("q");
|
|
// 快速筛选进 qf(同维度逗号分隔,值编码)
|
|
expect(toApiFilters({ q: "x" }, f, "", { resolution: ["2160p", "1080p"], promotion: ["50%"], hdr: [] }).qf).toBe(
|
|
"promotion:50%25;resolution:2160p,1080p",
|
|
);
|
|
expect(toApiFilters({ q: "x" }, f, "", {}).qf).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("库内状态文案", () => {
|
|
it("未收录 / 已建档 / 已收录 / 正在下载;收录版本满足与缺失", () => {
|
|
expect(inventoryBadge(inv({})).label).toBe("不在媒体库");
|
|
expect(inventoryBadge(inv({ state: "tracked" })).label).toBe("在媒体库 · 未下载");
|
|
expect(inventoryBadge(inv({ state: "owned", versionCount: 2 })).label).toBe("已入库 · 2 个版本");
|
|
expect(inventoryBadge(inv({ state: "downloading", downloadingCount: 1 })).label).toBe("正在下载 1 个");
|
|
expect(
|
|
slotLine(inv({ state: "owned", slotTotal: 2, slotSatisfied: 1, missing: [{ slotId: ID, name: "2160p" }] })),
|
|
).toBe("收录版本满足 1/2 · 缺「2160p」");
|
|
expect(slotLine(inv({}))).toBeNull();
|
|
});
|
|
|
|
it("Emby:任一实例有 = 已有;全部没有 = 没有;有失败 = 未知(不等于缺失)", () => {
|
|
const ms = (...st: ("present" | "not_found" | "unknown")[]): MediaServerInventory => ({
|
|
mediaEntityId: null,
|
|
partial: false,
|
|
instances: st.map((status, i) => ({
|
|
instanceId: `${ID.slice(0, -1)}${i}`,
|
|
name: `E${i}`,
|
|
status,
|
|
reason: null,
|
|
items: [],
|
|
observedAt: "2026-09-29T00:00:00.000Z",
|
|
})),
|
|
});
|
|
expect(mediaServerSummary(ms())).toBeNull();
|
|
expect(mediaServerSummary(ms("present"))?.label).toBe("Emby 已有");
|
|
expect(mediaServerSummary(ms("not_found", "not_found"))?.label).toBe("Emby 没有");
|
|
expect(mediaServerSummary(ms("not_found", "unknown"))?.label).toBe("Emby 未知");
|
|
});
|
|
});
|
|
|
|
describe("订阅入口位置", () => {
|
|
const src = (rel: string) => readFileSync(fileURLToPath(new URL(rel, import.meta.url)), "utf8");
|
|
it("搜索结果与 ⌘K 面板不能直接订阅", () => {
|
|
for (const f of ["./command-palette.tsx", "./search-sections.tsx", "./work-card.tsx"]) {
|
|
const s = src(f);
|
|
expect(s, f).not.toMatch(/useCreateSubscription|SubscriptionFormDialog|useSubscribeTmdb/);
|
|
}
|
|
});
|
|
it("作品详情页(本地 / 仅 TMDB)有订阅入口", () => {
|
|
for (const f of ["../../routes/console/library.$id.tsx", "../../routes/console/media.tmdb.$type.$id.tsx"]) {
|
|
expect(src(f), f).toMatch(/<SubscriptionFormDialog[\s\S]*initialTarget=/);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("可能不是这部作品的资源", () => {
|
|
const rel = (key: string, title: string): SearchRelease => ({
|
|
key,
|
|
sourceReleaseId: null,
|
|
siteId: ID,
|
|
siteName: "馒头",
|
|
siteReleaseId: key,
|
|
title,
|
|
subtitle: null,
|
|
category: { id: null, name: "音乐" },
|
|
sizeBytes: null,
|
|
seeders: 3,
|
|
leechers: 0,
|
|
snatched: null,
|
|
promotion: { type: "none", downloadFactor: 1, uploadFactor: 1, expiresAt: null },
|
|
free: false,
|
|
hr: false,
|
|
uploadedAt: null,
|
|
detailUrl: "http://x/",
|
|
observedAt: "2026-10-06T00:00:00.000Z",
|
|
parsed: {
|
|
titleCn: null,
|
|
titleEn: null,
|
|
year: 2023,
|
|
season: null,
|
|
episodes: null,
|
|
resolution: "1080p",
|
|
source: "BluRay-Disc",
|
|
videoCodec: null,
|
|
hdr: [],
|
|
releaseGroup: null,
|
|
},
|
|
mediaEntityId: null,
|
|
entityMatch: null,
|
|
entity: null,
|
|
inventory: null,
|
|
ruleVerdict: null,
|
|
});
|
|
const rows = [rel("a", "Channel U ESCL-6057~9 2023 BDMV 1080p FLAC-@U2")];
|
|
const render = (defaultOpen: boolean, total = 1) =>
|
|
renderToStaticMarkup(
|
|
createElement(
|
|
MemoryRouter,
|
|
null,
|
|
createElement(UnmatchedReleases, { rows, total, onOpen: () => {}, onDownload: () => {}, defaultOpen }),
|
|
),
|
|
);
|
|
|
|
it("默认折叠成一行「另有 N 条可能不是这部作品」,不列资源", () => {
|
|
const html = render(false);
|
|
expect(html).toContain("另有 1 条可能不是这部作品");
|
|
expect(html).toContain('aria-expanded="false"');
|
|
expect(html).not.toContain("Channel U");
|
|
});
|
|
|
|
it("展开后列出资源,仍有下载按钮", () => {
|
|
const html = render(true);
|
|
expect(html).toContain('aria-expanded="true"');
|
|
expect(html).toContain("Channel U ESCL-6057~9 2023 BDMV 1080p FLAC-@U2");
|
|
expect(html).toContain("下载");
|
|
});
|
|
|
|
it("没有时不渲染", () => {
|
|
expect(render(false, 0)).toBe("");
|
|
});
|
|
});
|