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:
commit
88f98386e8
21 files changed
+1955
-140
No files matched your search
@@ -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),
|
||||
|
||||
@@ -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,在其它筛选条件之后、按其它维度的已选条件计数)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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,计数按其它维度已选条件", () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user