feat(search): 按作品搜资源时判定每条结果是否属于这部作品
对照 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
This commit is contained in:
1 parent
f6d976a963
commit
9d7e20067b
9 files changed
+1560
-10
No files matched your search
@@ -57,6 +57,8 @@ import {
|
||||
// 行操作同站点工作台(库存叠加、下载、委托智能体)。耗时、查询词放进「技术信息」。
|
||||
// SEARCH-5(参考 MP 资源卡片):促销放标题前、规格拆成中性标签、关键数字定宽对齐、按作品分组,
|
||||
// 每行附规则判定(命中第几档 / 不满足什么 / 与库里版本比较,服务端 ruleVerdict)。
|
||||
// 按作品搜时,服务端判定不属于这部作品的资源(对照 MP match_torrent)不参与筛选与分面,
|
||||
// 在列表最后折叠为「另有 N 条可能不是这部作品」,展开后仍可下载。
|
||||
|
||||
/** URL 里的资源筛选(字符串形态,空串 = 不筛) */
|
||||
export type ReleaseFilterState = {
|
||||
@@ -247,14 +249,35 @@ export function ReleaseResults({
|
||||
<EmptyState
|
||||
plain
|
||||
icon={PackageSearch}
|
||||
title={filtered ? "没有符合筛选条件的资源" : "所选站点都没有找到资源"}
|
||||
description={filtered ? "放宽筛选条件再看。" : "换站点或换关键词再搜,或到站点页检查站点状态。"}
|
||||
title={
|
||||
filtered
|
||||
? "没有符合筛选条件的资源"
|
||||
: first?.unmatchedTotal
|
||||
? "没有找到这部作品的资源"
|
||||
: "所选站点都没有找到资源"
|
||||
}
|
||||
description={
|
||||
filtered
|
||||
? "放宽筛选条件再看。"
|
||||
: first?.unmatchedTotal
|
||||
? "下面是按名字搜到、但可能不是这部作品的资源。"
|
||||
: "换站点或换关键词再搜,或到站点页检查站点状态。"
|
||||
}
|
||||
className="py-12"
|
||||
/>
|
||||
) : (
|
||||
<ReleaseList rows={paged.items ?? []} onOpen={setSelected} onDownload={setDl} />
|
||||
)}
|
||||
{rows.length > 0 || res.hasNextPage ? <Pagination {...paged.pagination} /> : null}
|
||||
{first?.unmatchedTotal ? (
|
||||
<UnmatchedReleases
|
||||
key={JSON.stringify(api)}
|
||||
rows={first.unmatched ?? []}
|
||||
total={first.unmatchedTotal}
|
||||
onOpen={setSelected}
|
||||
onDownload={setDl}
|
||||
/>
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
{first ? (
|
||||
@@ -285,6 +308,42 @@ export function ReleaseResults({
|
||||
);
|
||||
}
|
||||
|
||||
/** 「另有 N 条可能不是这部作品」:默认收起,展开后与上面的资源行一样可查看、下载 */
|
||||
export function UnmatchedReleases({
|
||||
rows,
|
||||
total,
|
||||
onOpen,
|
||||
onDownload,
|
||||
defaultOpen = false,
|
||||
}: {
|
||||
rows: SearchRelease[];
|
||||
total: number;
|
||||
onOpen: (r: SearchRelease) => void;
|
||||
onDownload: (r: SearchRelease) => void;
|
||||
defaultOpen?: boolean;
|
||||
}) {
|
||||
const [open, setOpen] = useState(defaultOpen);
|
||||
if (!total) return null;
|
||||
return (
|
||||
<div className="border-t border-border/60">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-expanded={open}
|
||||
className="flex w-full cursor-pointer items-center gap-1.5 px-4 py-2.5 text-left text-caption text-muted-foreground transition-colors duration-micro hover:bg-foreground/[0.03] hover:text-foreground"
|
||||
>
|
||||
<ChevronDown className={cn("h-4 w-4 transition-transform duration-micro", open && "rotate-180")} />
|
||||
另有 {total} 条可能不是这部作品
|
||||
</button>
|
||||
{open ? (
|
||||
<div className="border-t border-border/60">
|
||||
<ReleaseList rows={rows} onOpen={onOpen} onDownload={onDownload} />
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
/** 部分站点失败 / 跳过:列出站点,其余结果照常 */
|
||||
function CoverageNotice({ page }: { page: ReleaseSearchPage }) {
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import type { MediaServerInventory, WorkInventory } from "@umt/contracts";
|
||||
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 } from "./release-results.tsx";
|
||||
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";
|
||||
|
||||
@@ -127,3 +130,70 @@ describe("订阅入口位置", () => {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
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("");
|
||||
});
|
||||
});
|
||||
Reference in new issue
Block a user