feat(search): align quick-filter facets with MP (SEARCH-4)
Add audioCodec, subtitle, dub, feature, year, category, size and seeders facets with disjunctive counts; parser records dub languages (audioLangs). Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
c3b25be676
commit
d9cf8f1c3a
10 files changed
+409
-5
No files matched your search
@@ -16,6 +16,15 @@ export const FACET_DIMS = [
|
||||
"releaseGroup",
|
||||
"season",
|
||||
"episode",
|
||||
// SEARCH-4:对齐 MP 过滤规则里的音频 / 中字 / 国语粤语配音 / 60 帧,再加年份、分类、大小、做种
|
||||
"audioCodec",
|
||||
"subtitle",
|
||||
"dub",
|
||||
"feature",
|
||||
"year",
|
||||
"category",
|
||||
"size",
|
||||
"seeders",
|
||||
] as const;
|
||||
export type FacetDim = (typeof FACET_DIMS)[number];
|
||||
|
||||
@@ -49,6 +58,21 @@ export interface FacetInput {
|
||||
};
|
||||
/** H&R 三态;缺省 / null 按未知 */
|
||||
hr?: boolean | "unknown" | null;
|
||||
/** 解析出的音轨(`TrueHD`、`Atmos 7.1`、`DDP 5.1`…);空 = 标题没写 */
|
||||
audio?: readonly string[];
|
||||
/** 字幕语言(zh / zh-Hans / zh-Hant / en / ja / ko);缺省 / 空 = 标题没写 */
|
||||
subtitles?: readonly string[] | null;
|
||||
/** 配音语言(cmn / yue / en);缺省 / 空 = 标题没写 */
|
||||
audioLangs?: readonly string[] | null;
|
||||
bitDepth?: number | null;
|
||||
fps?: number | null;
|
||||
/** 剪辑版(加长版、导演剪辑版、IMAX…) */
|
||||
cut?: string | null;
|
||||
year?: number | null;
|
||||
/** 站点分类名 */
|
||||
category?: string | null;
|
||||
sizeBytes?: number | null;
|
||||
seeders?: number | null;
|
||||
}
|
||||
|
||||
/** 固定档位(顺序即展示顺序);"other" 类档位只在有资源时列出 */
|
||||
@@ -92,10 +116,62 @@ const FIXED: Partial<Record<FacetDim, readonly { value: string; label: string; h
|
||||
{ value: "no", label: "无" },
|
||||
{ value: "unknown", label: "未知", hideEmpty: true },
|
||||
],
|
||||
audioCodec: [
|
||||
{ value: "Atmos", label: "Atmos" },
|
||||
{ value: "TrueHD", label: "TrueHD" },
|
||||
{ value: "DTS:X", label: "DTS:X", hideEmpty: true },
|
||||
{ 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 },
|
||||
],
|
||||
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 },
|
||||
],
|
||||
dub: [
|
||||
{ value: "cmn", label: "国语" },
|
||||
{ value: "yue", label: "粤语" },
|
||||
{ value: "en", label: "英语", hideEmpty: true },
|
||||
{ value: "unknown", label: "未标注", hideEmpty: true },
|
||||
],
|
||||
feature: [
|
||||
{ value: "10bit", label: "10bit", hideEmpty: true },
|
||||
{ value: "hfr", label: "50 帧以上", hideEmpty: true },
|
||||
{ value: "cut", label: "特别剪辑版", hideEmpty: true },
|
||||
],
|
||||
size: [
|
||||
{ value: "lt1", label: "1 GB 以下" },
|
||||
{ value: "1-5", label: "1–5 GB" },
|
||||
{ value: "5-10", label: "5–10 GB" },
|
||||
{ value: "10-20", label: "10–20 GB" },
|
||||
{ value: "20-50", label: "20–50 GB" },
|
||||
{ value: "50+", label: "50 GB 以上" },
|
||||
{ value: "unknown", label: "未知", hideEmpty: true },
|
||||
],
|
||||
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 },
|
||||
],
|
||||
};
|
||||
|
||||
/** 制作组只列计数最多的前 N 个(已选的总会列出) */
|
||||
export const RELEASE_GROUP_TOP = 20;
|
||||
/** 分类只列计数最多的前 N 个(已选的总会列出) */
|
||||
export const CATEGORY_TOP = 20;
|
||||
/** 一条资源最多展开多少集(防止异常集号范围撑爆计数) */
|
||||
const MAX_EPISODES_PER_RELEASE = 200;
|
||||
const MAX_SEASONS_PER_RELEASE = 50;
|
||||
@@ -155,6 +231,71 @@ export function hrBucket(hr: FacetInput["hr"]): string {
|
||||
return hr === true ? "yes" : hr === false ? "no" : "unknown";
|
||||
}
|
||||
|
||||
const AUDIO_BUCKET: Record<string, string> = {
|
||||
Atmos: "Atmos",
|
||||
TrueHD: "TrueHD",
|
||||
"DTS:X": "DTS:X",
|
||||
"DTS-HD MA": "DTS-HD",
|
||||
"DTS-HD HR": "DTS-HD",
|
||||
"DTS-HD": "DTS-HD",
|
||||
DTS: "DTS",
|
||||
DDP: "DDP",
|
||||
"E-AC3": "DDP",
|
||||
DD: "DD",
|
||||
AC3: "DD",
|
||||
AAC: "AAC",
|
||||
FLAC: "FLAC",
|
||||
LPCM: "LPCM",
|
||||
PCM: "LPCM",
|
||||
};
|
||||
|
||||
/** 音频编码档位(一条可有多条音轨);声道(`5.1`)、音轨数(`2Audios`)不算编码 */
|
||||
export function audioBuckets(audio: readonly string[] | undefined): string[] {
|
||||
const out = new Set<string>();
|
||||
for (const a of audio ?? []) {
|
||||
const codec = a.replace(/\s*\d\.\d$/, "").trim();
|
||||
if (!codec || /^\d+Audios?$/i.test(codec)) continue;
|
||||
out.add(AUDIO_BUCKET[codec] ?? "other");
|
||||
}
|
||||
return out.size ? [...out] : ["unknown"];
|
||||
}
|
||||
|
||||
/** 字幕档位:任何中文字幕都算「中文字幕」,简 / 繁另列 */
|
||||
export function subtitleBuckets(subs: readonly string[] | null | undefined): string[] {
|
||||
if (!subs?.length) return ["unknown"];
|
||||
const out = new Set<string>();
|
||||
for (const s of subs) {
|
||||
if (s.startsWith("zh")) out.add("zh");
|
||||
if (s === "zh-Hans" || s === "zh-Hant" || s === "en" || s === "ja" || s === "ko") out.add(s);
|
||||
}
|
||||
return out.size ? [...out] : ["unknown"];
|
||||
}
|
||||
|
||||
export function dubBuckets(langs: readonly string[] | null | undefined): string[] {
|
||||
const out = (langs ?? []).filter((l) => l === "cmn" || l === "yue" || l === "en");
|
||||
return out.length ? [...new Set(out)] : ["unknown"];
|
||||
}
|
||||
|
||||
export function featureBuckets(r: Pick<FacetInput, "bitDepth" | "fps" | "cut">): string[] {
|
||||
const out: string[] = [];
|
||||
if (r.bitDepth && r.bitDepth >= 10) out.push("10bit");
|
||||
if (r.fps && r.fps >= 50) out.push("hfr");
|
||||
if (r.cut) out.push("cut");
|
||||
return out;
|
||||
}
|
||||
|
||||
const GIB = 1024 ** 3;
|
||||
export function sizeBucket(bytes: number | null | undefined): string {
|
||||
if (bytes === null || bytes === undefined || !Number.isFinite(bytes) || bytes < 0) return "unknown";
|
||||
const g = bytes / GIB;
|
||||
return g < 1 ? "lt1" : g < 5 ? "1-5" : g < 10 ? "5-10" : g < 20 ? "10-20" : g < 50 ? "20-50" : "50+";
|
||||
}
|
||||
|
||||
export function seedersBucket(n: number | null | undefined): string {
|
||||
if (n === null || n === undefined || !Number.isFinite(n) || n < 0) return "unknown";
|
||||
return n === 0 ? "0" : n < 10 ? "1-9" : n < 50 ? "10-49" : "50+";
|
||||
}
|
||||
|
||||
function seasonValues(r: FacetInput): string[] {
|
||||
if (r.season === null || r.season === undefined) return [];
|
||||
const end =
|
||||
@@ -188,6 +329,14 @@ export function facetValues(r: FacetInput, now: number): Record<FacetDim, string
|
||||
releaseGroup: r.releaseGroup ? [r.releaseGroup] : [],
|
||||
season: seasonValues(r),
|
||||
episode: episodeValues(r),
|
||||
audioCodec: audioBuckets(r.audio),
|
||||
subtitle: subtitleBuckets(r.subtitles),
|
||||
dub: dubBuckets(r.audioLangs),
|
||||
feature: featureBuckets(r),
|
||||
year: r.year ? [String(r.year)] : [],
|
||||
category: r.category?.trim() ? [r.category.trim()] : [],
|
||||
size: [sizeBucket(r.sizeBytes)],
|
||||
seeders: [seedersBucket(r.seeders)],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -290,9 +439,11 @@ function sortDynamic(
|
||||
const n = (v: string) => (v === "pack" ? -1 : Number(v));
|
||||
return options.sort((a, b) => n(a.value) - n(b.value)).map(strip);
|
||||
}
|
||||
// 年份新的在前
|
||||
if (dim === "year") return options.sort((a, b) => Number(b.value) - Number(a.value)).map(strip);
|
||||
const sorted = options.sort((a, b) => b.total - a.total || a.label.localeCompare(b.label));
|
||||
if (dim !== "releaseGroup") return sorted.map(strip);
|
||||
const head = sorted.slice(0, top);
|
||||
if (dim !== "releaseGroup" && dim !== "category") return sorted.map(strip);
|
||||
const head = sorted.slice(0, dim === "category" ? CATEGORY_TOP : top);
|
||||
const extra = sorted.slice(top).filter((o) => chosen.has(norm(dim, o.value)));
|
||||
return [...head, ...extra].map(strip);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user