merge: fix/m-search-filter

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-06 16:01:46 +08:00
commit 88f98386e8
21 files changed
+1955 -140

No files matched your search

+4 -2
View File
@@ -3,14 +3,14 @@ import { NonNegInt } from "./common.ts";
// 资源列表快速筛选(SEARCH-3):GET /search/releases 与 GET /sites/{id}/releases 共用。
// 口径实现在 @umt/domain release-facets:同一维度多选为 OR,不同维度为 AND;
// 计数按其它维度的已选条件统计(0 = 在其它条件下没有结果)。
// 计数按其它维度的已选条件统计;选项只列计数不为 0 的值(已选的值总会列出)。
export const FacetCount = z.object({ value: z.string(), label: z.string(), count: NonNegInt });
export type FacetCount = z.infer<typeof FacetCount>;
/**
* 快速筛选参数 qf:`维度:值,值;维度:值`,值经 encodeURIComponent。
* 维度:resolution(2160p/1080p/720p/other)、videoCodec(H.265/H.264/AV1/other)、
* 维度:seasonEpisode(季集,MP 写法 `S01` / `S01-S02` / `S02 E14` / `S01 E25-E48`)、resolution(2160p/1080p/720p/other)、videoCodec(H.265/H.264/AV1/other)、
* releaseType(disc/remux/encode/webdl/hdtv/other)、site(站点 ID)、promotion(free/2x/50%/other/none)、
* hdr(DV/HDR10/HDR10+/other/SDR)、hr(yes/no/unknown)、releaseGroup(制作组名,不分大小写)、season(季号)、episode(集号 / pack 整季);
* SEARCH-4 起:audioCodec(Atmos/TrueHD/DTS:X/DTS-HD/DTS/DDP/DD/AAC/FLAC/LPCM/other/unknown)、
@@ -20,6 +20,8 @@ export type FacetCount = z.infer<typeof FacetCount>;
export const QuickFilterParam = z.string().max(4000).optional();
export const ReleaseQuickFacets = z.object({
/** 季集:整季在前、单集在后,季号新的在前(对照 MP);没有剧集资源时为空 */
seasonEpisode: z.array(FacetCount),
resolution: z.array(FacetCount),
videoCodec: z.array(FacetCount),
releaseType: z.array(FacetCount),
+7 -1
View File
@@ -342,8 +342,14 @@ export const ReleaseSearchPage = z
nextCursor: z.string().nullable(),
/** 筛选后的条数 */
total: NonNegInt,
/** 未筛选的全部条数 */
/** 未筛选的全部条数(按作品搜时只算属于这部作品的) */
unfilteredTotal: NonNegInt,
/**
* 按作品搜(entityId / tmdb)时判定不属于这部作品的资源(对照 MP match_torrent:识别到别的作品,
* 或名字、类型、年份、季集对不上);不参与筛选与分面,只在第一页给出(最多 200 条)。关键词搜索没有这两项
*/
unmatched: z.array(SearchRelease).optional(),
unmatchedTotal: NonNegInt.optional(),
/**
* sites / resolutions / sources:旧的筛选项计数(基于未筛选的全部结果);
* 其余为快速筛选分面(SEARCH-3,在其它筛选条件之后、按其它维度的已选条件计数)
+71 -32
View File
@@ -3,9 +3,12 @@
//
// 组合逻辑:同一维度多选为 OR,不同维度之间为 AND。
// 计数按"其它维度的已选条件"算(disjunctive facet):某个选项的计数 = 加上它之后本维度仍是 OR 时能看到的条数,
// 所以已选维度里的其它选项仍有计数,0 计数表示在其它条件下没有结果。
// 所以已选维度里的其它选项仍有计数。
// 选项只列结果里实际出现、且在其它条件下计数不为 0 的值(对照 MP useTorrentFilter.initOptions);已选的值总会列出。
export const FACET_DIMS = [
// 季集合并成一个维度(对照 MP season_episode):`S01`、`S01-S02`、`S02 E14`、`S01 E25-E48`
"seasonEpisode",
"resolution",
"videoCodec",
"releaseType",
@@ -75,19 +78,19 @@ export interface FacetInput {
seeders?: number | null;
}
/** 固定档位(顺序即展示顺序);"other" 类档位只在有资源时列出 */
const FIXED: Partial<Record<FacetDim, readonly { value: string; label: string; hideEmpty?: boolean }[]>> = {
/** 固定档位(顺序即展示顺序与选项名);只列出有资源的档位 */
const FIXED: Partial<Record<FacetDim, readonly { value: string; label: string }[]>> = {
resolution: [
{ value: "2160p", label: "2160p" },
{ value: "1080p", label: "1080p" },
{ value: "720p", label: "720p" },
{ value: "other", label: "其他", hideEmpty: true },
{ value: "other", label: "其他" },
],
videoCodec: [
{ value: "H.265", label: "H.265" },
{ value: "H.264", label: "H.264" },
{ value: "AV1", label: "AV1" },
{ value: "other", label: "其他", hideEmpty: true },
{ value: "other", label: "其他" },
],
releaseType: [
{ value: "disc", label: "原盘" },
@@ -95,60 +98,60 @@ const FIXED: Partial<Record<FacetDim, readonly { value: string; label: string; h
{ value: "encode", label: "蓝光压制" },
{ value: "webdl", label: "WEB-DL" },
{ value: "hdtv", label: "HDTV" },
{ value: "other", label: "其他", hideEmpty: true },
{ value: "other", label: "其他" },
],
promotion: [
{ value: "free", label: "免费" },
{ value: "2x", label: "2x" },
{ value: "50%", label: "50%" },
{ value: "other", label: "其他", hideEmpty: true },
{ value: "other", label: "其他" },
{ value: "none", label: "无" },
],
hdr: [
{ value: "DV", label: "DV" },
{ value: "HDR10", label: "HDR10" },
{ value: "HDR10+", label: "HDR10+" },
{ value: "other", label: "其他 HDR", hideEmpty: true },
{ value: "other", label: "其他 HDR" },
{ value: "SDR", label: "SDR" },
],
hr: [
{ value: "yes", label: "有" },
{ value: "no", label: "无" },
{ value: "unknown", label: "未知", hideEmpty: true },
{ value: "unknown", label: "未知" },
],
audioCodec: [
{ value: "Atmos", label: "Atmos" },
{ value: "TrueHD", label: "TrueHD" },
{ value: "DTS:X", label: "DTS:X", hideEmpty: true },
{ value: "DTS:X", label: "DTS:X" },
{ value: "DTS-HD", label: "DTS-HD MA" },
{ value: "DTS", label: "DTS" },
{ value: "DDP", label: "DD+" },
{ value: "DD", label: "DD" },
{ value: "AAC", label: "AAC" },
{ value: "FLAC", label: "FLAC", hideEmpty: true },
{ value: "LPCM", label: "LPCM", hideEmpty: true },
{ value: "other", label: "其他", hideEmpty: true },
{ value: "unknown", label: "未标注", hideEmpty: true },
{ value: "FLAC", label: "FLAC" },
{ value: "LPCM", label: "LPCM" },
{ value: "other", label: "其他" },
{ value: "unknown", label: "未标注" },
],
subtitle: [
{ value: "zh", label: "中文字幕" },
{ value: "zh-Hans", label: "简体" },
{ value: "zh-Hant", label: "繁体" },
{ value: "en", label: "英文" },
{ value: "ja", label: "日文", hideEmpty: true },
{ value: "ko", label: "韩文", hideEmpty: true },
{ value: "unknown", label: "未标注", hideEmpty: true },
{ value: "ja", label: "日文" },
{ value: "ko", label: "韩文" },
{ value: "unknown", label: "未标注" },
],
dub: [
{ value: "cmn", label: "国语" },
{ value: "yue", label: "粤语" },
{ value: "en", label: "英语", hideEmpty: true },
{ value: "unknown", label: "未标注", hideEmpty: true },
{ value: "en", label: "英语" },
{ value: "unknown", label: "未标注" },
],
feature: [
{ value: "10bit", label: "10bit", hideEmpty: true },
{ value: "hfr", label: "50 帧以上", hideEmpty: true },
{ value: "cut", label: "特别剪辑版", hideEmpty: true },
{ value: "10bit", label: "10bit" },
{ value: "hfr", label: "50 帧以上" },
{ value: "cut", label: "特别剪辑版" },
],
size: [
{ value: "lt1", label: "1 GB 以下" },
@@ -157,14 +160,14 @@ const FIXED: Partial<Record<FacetDim, readonly { value: string; label: string; h
{ value: "10-20", label: "10–20 GB" },
{ value: "20-50", label: "20–50 GB" },
{ value: "50+", label: "50 GB 以上" },
{ value: "unknown", label: "未知", hideEmpty: true },
{ value: "unknown", label: "未知" },
],
seeders: [
{ value: "0", label: "无人做种" },
{ value: "1-9", label: "1–9" },
{ value: "10-49", label: "10–49" },
{ value: "50+", label: "50 以上" },
{ value: "unknown", label: "未知", hideEmpty: true },
{ value: "unknown", label: "未知" },
],
};
@@ -305,6 +308,37 @@ function seasonValues(r: FacetInput): string[] {
return out;
}
const pad2 = (n: number) => String(n).padStart(2, "0");
/** 季集(MP 的 season_episode 写法):`S01`、`S01-S03`、`S01 E05`、`S01 E01-E12`;没有季集信息为空 */
export function seasonEpisodeValue(r: Pick<FacetInput, "season" | "seasonEnd" | "episodes">): string | null {
if (r.season === null || r.season === undefined) return null;
const s = r.seasonEnd && r.seasonEnd > r.season ? `S${pad2(r.season)}-S${pad2(r.seasonEnd)}` : `S${pad2(r.season)}`;
if (!r.episodes) return s;
const { start, end } = r.episodes;
return end > start ? `${s} E${pad2(start)}-E${pad2(end)}` : `${s} E${pad2(start)}`;
}
const SEASON_EPISODE = /^S(\d+)(?:-S(\d+))?(?:\s+E(\d+)(?:-E(\d+))?)?$/;
/**
* 季集选项排序(照 MP sortSeasonOptions):整季在前、单集在后;整季按季号新→旧,
* 单集按季号新→旧、再按最大集号大→小、起始集号大→小;认不出的写法排在最后。
*/
export function compareSeasonEpisode(a: string, b: string): number {
const key = (v: string) => {
const m = SEASON_EPISODE.exec(v);
if (!m) return { ok: false, whole: false, season: 0, ep: 0, maxEp: 0 };
const ep = m[3] ? Number(m[3]) : 0;
return { ok: true, whole: !m[3], season: Number(m[1]), ep, maxEp: m[4] ? Number(m[4]) : ep };
};
const x = key(a);
const y = key(b);
if (x.ok !== y.ok) return x.ok ? -1 : 1;
if (x.whole !== y.whole) return x.whole ? -1 : 1;
return y.season - x.season || y.maxEp - x.maxEp || y.ep - x.ep;
}
function episodeValues(r: FacetInput): string[] {
if (r.episodes) {
const { start, end } = r.episodes;
@@ -318,7 +352,9 @@ function episodeValues(r: FacetInput): string[] {
/** 一条资源在各维度上的档位(制作组按原名,匹配时不分大小写) */
export function facetValues(r: FacetInput, now: number): Record<FacetDim, string[]> {
const se = seasonEpisodeValue(r);
return {
seasonEpisode: se ? [se] : [],
resolution: [resolutionBucket(r.resolution)],
videoCodec: [codecBucket(r.videoCodec)],
releaseType: [releaseTypeBucket(r.source)],
@@ -367,7 +403,7 @@ export interface FacetOutcome<T> {
/**
* 按已选条件筛选并统计各维度计数。
* 选项来自全部输入条目(固定档位总会列出,"其他"类只在有资源时列出);已选但当前没有资源的值也会列出(计数 0)。
* 选项只列在其它维度已选条件下计数不为 0 的值(固定档位按固定顺序);已选的值总会列出(当前没有资源时计数 0)。
*/
export function applyFacets<T>(
items: readonly T[],
@@ -400,15 +436,17 @@ export function applyFacets<T>(
let options: FacetOption[];
if (fixed) {
options = fixed
.filter((f) => !f.hideEmpty || universe.has(f.value) || chosen.has(f.value))
.filter((f) => (counts.get(f.value) ?? 0) > 0 || chosen.has(f.value))
.map((f) => ({ value: f.value, label: f.label, count: counts.get(f.value) ?? 0 }));
} else {
options = [...universe.entries()].map(([value, u]) => ({
value: dim === "releaseGroup" ? u.label : value,
label: u.label,
count: counts.get(value) ?? 0,
total: u.total,
}));
options = [...universe.entries()]
.filter(([value]) => (counts.get(value) ?? 0) > 0 || chosen.has(value))
.map(([value, u]) => ({
value: dim === "releaseGroup" ? u.label : value,
label: u.label,
count: counts.get(value) ?? 0,
total: u.total,
}));
options = sortDynamic(
dim,
options as (FacetOption & { total: number })[],
@@ -435,6 +473,7 @@ function sortDynamic(
label: o.label,
count: o.count,
});
if (dim === "seasonEpisode") return options.sort((a, b) => compareSeasonEpisode(a.value, b.value)).map(strip);
if (dim === "season" || dim === "episode") {
const n = (v: string) => (v === "pack" ? -1 : Number(v));
return options.sort((a, b) => n(a.value) - n(b.value)).map(strip);
+35 -1
View File
@@ -45,6 +45,8 @@ type Draft = ParsedRelease & {
_fin?: boolean;
_disc?: boolean;
_remux?: boolean;
/** 标题里有 BDRip / BRRip(压制) */
_rip?: boolean;
/** `{[k=v;…]}` 显式身份块里的季/集/类型,finalize 时覆盖推断值 */
_explicit?: { season?: number; seasonEnd?: number; episodes?: EpisodeRange; tv?: boolean };
};
@@ -746,6 +748,7 @@ function walkTokens(tokens: string[], d: Draft): void {
const cnTitle = cn.join(" ").trim();
if (enTitle) d.titleEn = enTitle;
if (cnTitle) d.titleCn = cnTitle;
if (ripWord) d._rip = true;
if (d._remux) d.source = "REMUX";
else if (d._disc) d.source = "BluRay-Disc";
else if (blurayWord && discCodec && !encodeCodec && !ripWord && !d.encoder && sources.size === 1) {
@@ -1077,9 +1080,40 @@ export function parseRelease(input: string, opts: ParseOptions = {}): ParsedRele
}
}
// 14. 原盘线索:标题的编码字样不足以判定时,参考副标题 / 简介里的明确说法与无损 PCM 音轨
refineDisc(d, s, subtitle);
return finalize(d, opts);
}
/** 明确说是原盘:原盘(不含「原盘压制」)、BDMV、自购自抓 / 自抓自扫、Blu-ray Disc、DIY(原盘 DIY) */
const DISC_CUE =
/原[盘盤](?![\s::]*(?:压|壓|rip|转|轉))|BDMV|自[购購]自抓|自抓自[扫掃]|Blu-?ray[\s.]+Disc|(?<![A-Za-z])DIY(?![A-Za-z])/i;
/** 明确说是压制或其他来源 */
const ENCODE_CUE =
/自[压壓]|[压壓]制|重[编編]码|(?<![A-Za-z])(?:[xX]\.?26[45]|BDRip|BRRip|WEB-?DL|WEBRip|HDTV|Remux)(?![A-Za-z])/i;
/**
* 原盘补判(对照 MP 内置规则 BLU 之外的站点惯例):标题写成 `Bluray H.264` 这类压制字样、但副标题写「自购自抓自扫」,
* 或带 LPCM / PCM 音轨又没有 x264 / x265 / BDRip / 压制说法的,按原盘。只在来源是 BluRay(或标题没写来源但有分辨率)时补判,
* WEB / HDTV / Remux 不动。
*/
function refineDisc(d: Draft, title: string, subtitle: string | undefined): void {
// 单个视频文件(mkv / mp4 …)不会是原盘
if (d.isFile || d._rip || d.encoder || d._remux || d.source === "BluRay-Disc") return;
if (d.source !== "BluRay" && !(d.source === undefined && d.resolution)) return;
const text = subtitle ? `${title} | ${subtitle}` : title;
if (ENCODE_CUE.test(text)) return;
const pcm = d.source === "BluRay" && d.audio.some((a) => /^L?PCM\b/i.test(a));
if (DISC_CUE.test(text)) {
d.source = "BluRay-Disc";
d.evidence.push("source:disc_hint");
} else if (pcm) {
d.source = "BluRay-Disc";
d.evidence.push("source:disc_pcm");
}
}
function applyCnMarkers(d: Draft, cm: CnMarkers, prefix: string): void {
const ev = d.evidence;
const note = (k: string) => {
@@ -1171,7 +1205,7 @@ function finalize(d: Draft, opts: ParseOptions): ParsedRelease {
d.title = d.titleCn ?? d.titleEn;
d.confidence = confidenceOf(d);
const { _complete, _fin, _disc, _remux, _explicit, ...out } = d;
const { _complete, _fin, _disc, _remux, _rip, _explicit, ...out } = d;
return out;
}
+63 -28
View File
@@ -2,12 +2,14 @@ import { describe, expect, it } from "vitest";
import {
applyFacets,
audioBuckets,
compareSeasonEpisode,
type FacetInput,
formatFacetParam,
parseFacetParam,
parseRelease,
promotionBuckets,
releaseTypeBucket,
seasonEpisodeValue,
seedersBucket,
sizeBucket,
subtitleBuckets,
@@ -76,8 +78,9 @@ describe("release-facets", () => {
expect(counts(r.facets.videoCodec)).toEqual({ "H.265": 2, "H.264": 1, AV1: 1, other: 1 });
expect(counts(r.facets.releaseType)).toEqual({ disc: 1, remux: 1, encode: 1, webdl: 1, hdtv: 1 });
expect(r.facets.releaseType.map((o) => o.label)).toEqual(["原盘", "Remux", "蓝光压制", "WEB-DL", "HDTV"]);
expect(counts(r.facets.hdr)).toEqual({ DV: 1, HDR10: 2, "HDR10+": 0, SDR: 3 });
expect(counts(r.facets.promotion)).toEqual({ free: 2, "2x": 1, "50%": 0, none: 3 });
// 结果里没有的档位不列出(HDR10+、50%)
expect(counts(r.facets.hdr)).toEqual({ DV: 1, HDR10: 2, SDR: 3 });
expect(counts(r.facets.promotion)).toEqual({ free: 2, "2x": 1, none: 3 });
expect(r.facets.site).toEqual([
{ value: "a", label: "站 A", count: 3 },
{ value: "b", label: "站 B", count: 2 },
@@ -88,24 +91,25 @@ describe("release-facets", () => {
{ value: "FraMeSToR", label: "FraMeSToR", count: 1 },
]);
expect(r.facets.season).toEqual([]);
expect(r.facets.seasonEpisode).toEqual([]);
});
it("同维度 OR、不同维度 AND;计数按其它维度的已选条件", () => {
const r = run({ resolution: ["2160p", "1080p"], videoCodec: ["H.265"] });
expect(ids(r)).toEqual(["1", "2"]);
// 分辨率的计数只受编码条件影响:1080p 在 H.265 下没有结果 → 0
expect(counts(r.facets.resolution)).toEqual({ "2160p": 2, "1080p": 0, "720p": 0, other: 0 });
// 编码计数只受分辨率条件影响
expect(counts(r.facets.videoCodec)).toEqual({ "H.265": 2, "H.264": 1, AV1: 0, other: 0 });
// 其它维度按全部条件
expect(counts(r.facets.releaseType)).toMatchObject({ remux: 1, webdl: 1, encode: 0 });
// 分辨率的计数只受编码条件影响:1080p 在 H.265 下没有结果,但已选,仍列出(计数 0);720p / 其他不列
expect(counts(r.facets.resolution)).toEqual({ "2160p": 2, "1080p": 0 });
// 编码计数只受分辨率条件影响:在 2160p / 1080p 下没有 AV1 / 其他,不列
expect(counts(r.facets.videoCodec)).toEqual({ "H.265": 2, "H.264": 1 });
// 其它维度按全部条件:计数为 0 的档位不列
expect(counts(r.facets.releaseType)).toEqual({ remux: 1, webdl: 1 });
expect(ids(run({ site: ["b"], releaseType: ["disc", "webdl"] }))).toEqual(["2", "5"]);
expect(ids(run({ releaseGroup: ["flux"] }))).toEqual(["2", "3"]);
expect(ids(run({ hdr: ["SDR"] }))).toEqual(["3", "4", "5"]);
expect(ids(run({ promotion: ["2x"] }))).toEqual(["4"]);
});
it("H&R 三态分面:缺省按未知;未知档只在有资源时列出", () => {
it("H&R 三态分面:缺省按未知;只列有资源的档", () => {
const hrItems = [mk("y", { hr: true }), mk("n", { hr: false }), mk("n2", { hr: false }), mk("u")];
const r = applyFacets(hrItems, (x) => x, {}, { now: NOW });
expect(r.facets.hr).toEqual([
@@ -115,7 +119,7 @@ describe("release-facets", () => {
]);
expect(ids(applyFacets(hrItems, (x) => x, { hr: ["no", "unknown"] }, { now: NOW }))).toEqual(["n", "n2", "u"]);
const known = applyFacets([mk("y", { hr: true })], (x) => x, {}, { now: NOW });
expect(known.facets.hr.map((o) => o.value)).toEqual(["yes", "no"]);
expect(known.facets.hr.map((o) => o.value)).toEqual(["yes"]);
});
it("已选但没有结果的值仍列出(计数 0)", () => {
@@ -150,14 +154,56 @@ describe("release-facets", () => {
{ value: "2", label: "S02", count: 1 },
{ value: "3", label: "S03", count: 1 },
]);
expect(r.facets.episode).toEqual([
{ value: "pack", label: "整季", count: 1 },
{ value: "2", label: "E02", count: 0 },
{ value: "3", label: "E03", count: 0 },
]);
// 在 S02 下单集没有结果,不列
expect(r.facets.episode).toEqual([{ value: "pack", label: "整季", count: 1 }]);
expect(ids(applyFacets(tv, (x) => x, { season: ["1"], episode: ["3"] }, { now: NOW }))).toEqual(["s1e2"]);
});
it("季集(MP season_episode):一个维度,整季在前、单集在后,季号新的在前", () => {
const tv = [
mk("s1", { season: 1, episodes: null }),
mk("s1b", { season: 1, episodes: null }),
mk("s2", { season: 2, episodes: null }),
mk("s1-3", { season: 1, seasonEnd: 3, episodes: null }),
mk("s1e25", { season: 1, episodes: { start: 25, end: 48 } }),
mk("s2e14", { season: 2, episodes: { start: 14, end: 14 } }),
mk("s2e1", { season: 2, episodes: { start: 1, end: 1 } }),
mk("s2e14-15", { season: 2, episodes: { start: 14, end: 15 } }),
mk("movie"),
];
const r = applyFacets(tv, (x) => x, {}, { now: NOW });
expect(r.facets.seasonEpisode.map((o) => [o.value, o.count])).toEqual([
["S02", 1],
["S01", 2],
["S01-S03", 1],
["S02 E14-E15", 1],
["S02 E14", 1],
["S02 E01", 1],
["S01 E25-E48", 1],
]);
expect(r.facets.seasonEpisode[0]?.label).toBe("S02");
expect(ids(applyFacets(tv, (x) => x, { seasonEpisode: ["S01", "S02 E14"] }, { now: NOW }))).toEqual([
"s1",
"s1b",
"s2e14",
]);
// 计数按其它维度已选条件;在其它条件下为 0 的不列
const free = applyFacets(
tv.map((t) => (t.id === "s2" ? { ...t, promotion: { ...none, type: "free", downloadFactor: 0 } } : t)),
(x) => x,
{ promotion: ["free"] },
{ now: NOW },
);
expect(free.facets.seasonEpisode).toEqual([{ value: "S02", label: "S02", count: 1 }]);
expect(seasonEpisodeValue({ season: null, episodes: { start: 1, end: 2 } })).toBeNull();
expect(["S01 E02", "bogus", "S10", "S02 E10-E12"].sort(compareSeasonEpisode)).toEqual([
"S10",
"S02 E10-E12",
"S01 E02",
"bogus",
]);
});
it("促销与版本类型档位", () => {
expect(promotionBuckets({ type: "2x50%", downloadFactor: 0.5, uploadFactor: 2, expiresAt: null }, NOW)).toEqual([
"2x",
@@ -227,10 +273,7 @@ describe("release-facets", () => {
Atmos: 1,
TrueHD: 1,
"DTS-HD": 1,
DTS: 0,
DDP: 1,
DD: 0,
AAC: 0,
unknown: 1,
});
expect(r.facets.audioCodec.find((o) => o.value === "DDP")?.label).toBe("DD+");
@@ -246,16 +289,8 @@ describe("release-facets", () => {
["电影", 2],
["剧集", 1],
]);
expect(counts(r.facets.size)).toEqual({
lt1: 0,
"1-5": 1,
"5-10": 1,
"10-20": 0,
"20-50": 0,
"50+": 1,
unknown: 1,
});
expect(counts(r.facets.seeders)).toEqual({ "0": 1, "1-9": 0, "10-49": 1, "50+": 1, unknown: 1 });
expect(counts(r.facets.size)).toEqual({ "1-5": 1, "5-10": 1, "50+": 1, unknown: 1 });
expect(counts(r.facets.seeders)).toEqual({ "0": 1, "10-49": 1, "50+": 1, unknown: 1 });
});
it("新维度组合:同维度 OR、跨维度 AND,计数按其它维度已选条件", () => {
+107
View File
@@ -627,6 +627,113 @@ describe("MP 差分补迁写法", () => {
});
});
// 原盘补判(SEARCH 准确度):站点搜索「药屋少女的呢喃」的真实结果。标题写成压制字样、但副标题说「自购自抓自扫」的是原盘;
// LPCM / PCM 音轨且没有 x264 / x265 / BDRip / 压制说法的也是原盘。普通压制、WEB-DL、Remux 不受影响。
const DISC_HINTS: Sample[] = [
{
id: "DISC-sub-self-rip",
input: "Kusuriya no Hitorigoto 2025 S02 1080p Bluray H.264 LPCM-@U2",
subtitle: "自购自抓自扫 | Team HAYAKU | Animate特典附 | TBR35041D 药屋少女的呢喃 第二季",
expect: { source: "BluRay-Disc", isEncode: false },
},
{
id: "DISC-sub-bdmv",
input: "The Apothecary Diaries 2025 S02 1080p Blu-ray H.264-GRP",
subtitle: "药屋少女的呢喃 第二季 BDMV Vol.1",
expect: { source: "BluRay-Disc" },
},
{
id: "DISC-sub-yuanpan",
input: "Some Show S01 1080p BluRay H.264 DTS-HD MA 5.1-GRP",
subtitle: "某剧 第一季 原盘 中字",
expect: { source: "BluRay-Disc" },
},
{
id: "DISC-pcm",
input: "Some Movie 2020 1080p BluRay LPCM 2.0-GRP",
expect: { source: "BluRay-Disc", isEncode: false },
},
// 已按编码字样判成原盘的不变
{
id: "DISC-avc-lpcm",
input: "Kusuriya no Hitorigoto S02 2025 Blu-ray 1080p AVC LPCM 2Audio-@U2",
subtitle: "Team HAYAKU | Animate全卷特典附 药屋少女的呢喃 第二季",
expect: { source: "BluRay-Disc" },
},
{
id: "DISC-bdmv-rip-welcome",
input: "The Apothecary Diaries Vol.3 2025 BDMV 1080p AVC LPCM 2.0-Arri-RAWS@U2",
subtitle: "U2 Exclusive, RIPs welcomed with credits | 制作RIP请注明出处 | 药屋少女的呢喃/药师少女的独语 S2",
expect: { source: "BluRay-Disc" },
},
// 反例:压制
{
id: "ENC-self-encode",
input: "Kusuriya no Hitorigoto 2023 BDRip 1080p AVC FLAC@U2",
subtitle: "药师少女的呢喃/Kusuriya no Hitorigoto/薬屋のひとりごと 1080p 自压(附相关专辑) TV 01-24 Fin+SP",
expect: { source: "BluRay", isEncode: true },
},
{
id: "ENC-x265",
input: "The Apothecary Diaries S01 2023 1080p BluRay x265 10bit FLAC 2.0-ADE",
subtitle: "药屋少女的呢喃 / 药师少女的独语 / The Apothecary Diaries / 薬屋のひとりごと 【简日|繁日|简|繁|日字幕】",
expect: { source: "BluRay", isEncode: true },
},
{
id: "ENC-x264",
input: "Kusuriya no Hitorigoto S01 Complete 2023 1080p Blu-ray x264 FLAC-jsum@U2",
subtitle: "药屋少女的呢喃 / 药师少女的独语 / 薬屋のひとりごと",
expect: { source: "BluRay", isEncode: true },
},
{
id: "ENC-av1",
input: "Kusuriya no Hitorigoto S02 1080p BluRay AV1 OPUS 2.0-7³ACG@OurBits",
subtitle: "药师少女的独语第2季/薬屋のひとりごと 第2期 | 01-24(25-48) 日语+简繁字幕",
expect: { source: "BluRay", isEncode: true },
},
{ id: "ENC-x264-lpcm", input: "Some Movie 2020 1080p BluRay x264 LPCM 2.0-GRP", expect: { source: "BluRay" } },
{
id: "ENC-from-disc",
input: "Some Movie 2020 1080p BluRay H.264 FLAC-GRP",
subtitle: "基于原盘压制 简繁字幕",
expect: { source: "BluRay" },
},
{
id: "ENC-pcm-encode-note",
input: "Some Movie 2020 1080p BluRay H.264 LPCM-GRP",
subtitle: "自压 内封字幕",
expect: { source: "BluRay" },
},
// 反例:其他来源不受副标题影响
{
id: "WEB-sub-yuanpan",
input: "Some Movie 2020 1080p WEB-DL H.264 AAC-GRP",
subtitle: "原盘 DIY 还没出,先发 WEB",
expect: { source: "WEB-DL" },
},
{
id: "REMUX-diy",
input: "The Apothecary Diaries S01 2023 1080p BluRay Remux AVC FLAC 2.0-ADE",
subtitle: "药屋少女的呢喃 【DIY / 双语简繁字幕】",
expect: { source: "REMUX" },
},
// 标题里写着 2160P 的非发布名(播放器解码信息):按字面解析,不猜
{
id: "WEB-2160-literal",
input:
"The Apothecary Diaries 2023 S01E24 2160P TV WEB-DL MPEG-4 AVC1 - Native D3D9 DXVA Decoder VLD AAC LC -Kisssub",
subtitle:
"药屋少女的呢喃 薬屋のひとりごと (2023) 2160P TVS01 [01-24](AI增强1080P转2160P) AVC1 @5160kbps AAC @128kbps",
expect: { source: "WEB-DL", resolution: "2160p" },
},
];
describe("原盘补判(副标题线索 / PCM 音轨)", () => {
for (const s of DISC_HINTS) {
it(`${s.id} ${s.input}`, () => check(parseRelease(s.input, s.subtitle ? { subtitle: s.subtitle } : {}), s.expect));
}
});
describe("parseRelease 样本表", () => {
it("样本数量 ≥ 60", () => {
expect(TITLES.length).toBeGreaterThanOrEqual(60);