- H&R becomes a quick-filter facet (yes/no/unknown, with counts) on search results and the site resources tab; drop the old free/H&R selects, keep size range and sort on the same row (mobile: drawer), single clear button - legacy free/hr URL params migrate into quick filters; stored selections keep the same localStorage key - search timing and query terms move into TechInfo - library episode versions: click the whole row to expand, versions listed as sibling rows, details only under the selected version, raw values in TechInfo - slot comparison gaps get reason codes (matchGapText in contracts reasons.ts): "可换成 2160p", "缺 HDR 版本" Co-Authored-By: Claude Opus 5.5 <[email protected]>
127 lines
4.8 KiB
TypeScript
127 lines
4.8 KiB
TypeScript
import { z } from "zod";
|
|
import { AssetStatus, LocationStatus } from "./assets.ts";
|
|
import { Id, JsonObject, NonNegInt, Timestamp } from "./common.ts";
|
|
import { MediaKind } from "./content.ts";
|
|
|
|
// LIBV-1 入库版本(GET /library/{id}/versions):一集(或一部电影)可挂多个本地文件,每个文件是一个版本。
|
|
// 规格来自文件名解析 + ffprobe(domain versionSpecOf);季 / 作品给汇总;
|
|
// 配了版本目标时每集给出比对结果 matches(目前只比一个目标,数组形态留给多目标)。
|
|
|
|
export const VersionAudio = z.object({
|
|
codec: z.string().optional(),
|
|
channels: z.string().optional(),
|
|
language: z.string().optional(),
|
|
});
|
|
export type VersionAudio = z.infer<typeof VersionAudio>;
|
|
|
|
export const AssetVersion = z.object({
|
|
assetId: Id,
|
|
status: AssetStatus,
|
|
resolution: z.string().nullable(),
|
|
/** REMUX / BluRay(压制)/ BluRay-Disc(原盘)/ WEB-DL / WEBRip / HDTV … */
|
|
source: z.string().nullable(),
|
|
isEncode: z.boolean(),
|
|
videoCodec: z.string().nullable(),
|
|
bitDepth: z.number().int().nullable(),
|
|
hdr: z.array(z.string()),
|
|
audio: z.array(VersionAudio),
|
|
subtitleLanguages: z.array(z.string()),
|
|
releaseGroup: z.string().nullable(),
|
|
/** 剪辑版本(导演剪辑版等),无标记为 null */
|
|
edition: z.string().nullable(),
|
|
sizeBytes: NonNegInt.nullable(),
|
|
durationSec: NonNegInt.nullable(),
|
|
fileName: z.string(),
|
|
relativePath: z.string().nullable(),
|
|
/** 库根本地路径 + 相对路径;库根未知时为 null */
|
|
absolutePath: z.string().nullable(),
|
|
libraryRootId: Id.nullable(),
|
|
locationStatus: LocationStatus.nullable(),
|
|
/** 入库时间(资产登记时间) */
|
|
organizedAt: Timestamp,
|
|
downloadTaskId: Id.nullable(),
|
|
sourceReleaseId: Id.nullable(),
|
|
/** 规格来源:name = 文件名,probe = ffprobe */
|
|
specFrom: z.array(z.enum(["name", "probe"])),
|
|
/** delivery 已按当前口径写回;false = 本次现算,可跑回填脚本 */
|
|
specStored: z.boolean(),
|
|
/** ffprobe 原始摘要(技术信息折叠里展示) */
|
|
probe: JsonObject.nullable(),
|
|
/** 覆盖的内容单元(合集文件覆盖多集) */
|
|
unitIds: z.array(Id),
|
|
});
|
|
export type AssetVersion = z.infer<typeof AssetVersion>;
|
|
|
|
export const VersionSpecGroup = z.object({
|
|
resolution: z.string().nullable(),
|
|
source: z.string().nullable(),
|
|
hdr: z.array(z.string()),
|
|
count: NonNegInt,
|
|
});
|
|
export const VersionSummary = z.object({
|
|
versionCount: NonNegInt,
|
|
sizeBytes: NonNegInt,
|
|
/** 按 分辨率 + 来源 + HDR 分组,多的在前 */
|
|
specs: z.array(VersionSpecGroup),
|
|
});
|
|
export type VersionSummary = z.infer<typeof VersionSummary>;
|
|
|
|
export const UnitMatchState = z.enum(["satisfied", "missing", "upgradeable"]);
|
|
export type UnitMatchState = z.infer<typeof UnitMatchState>;
|
|
export const UnitMatch = z.object({
|
|
slotId: Id.nullable(),
|
|
state: UnitMatchState,
|
|
assetId: Id.nullable(),
|
|
/** 比对说明原文(界面放进技术信息) */
|
|
reasons: z.array(z.string()),
|
|
/** 差距原因码(`version_resolution:2160p` …),人话见 reasons.ts matchGapText */
|
|
codes: z.array(z.string()),
|
|
});
|
|
export type UnitMatch = z.infer<typeof UnitMatch>;
|
|
export const MatchCounts = z.object({ satisfied: NonNegInt, missing: NonNegInt, upgradeable: NonNegInt });
|
|
export type MatchCounts = z.infer<typeof MatchCounts>;
|
|
|
|
export const EpisodeVersions = z.object({
|
|
unitId: Id,
|
|
episodeNumber: z.number().int(),
|
|
segment: z.string().nullable(),
|
|
versions: z.array(AssetVersion),
|
|
/** 与各版本目标的比对;没有版本目标时不出现 */
|
|
matches: z.array(UnitMatch).optional(),
|
|
});
|
|
export type EpisodeVersions = z.infer<typeof EpisodeVersions>;
|
|
|
|
export const SeasonVersions = z.object({
|
|
seasonNumber: z.number().int(),
|
|
unitId: Id.nullable(),
|
|
/** 该季总集数(元数据);未知为 null */
|
|
episodeTotal: NonNegInt.nullable(),
|
|
/** 至少有一个版本的集数 */
|
|
episodesWithVersion: NonNegInt,
|
|
summary: VersionSummary,
|
|
episodes: z.array(EpisodeVersions),
|
|
/** 按第一个版本目标统计;没有版本目标时不出现 */
|
|
matchCounts: MatchCounts.optional(),
|
|
});
|
|
export type SeasonVersions = z.infer<typeof SeasonVersions>;
|
|
|
|
export const LibraryVersionsQuery = z.object({
|
|
/** 比对哪个版本目标;缺省取作品的主版本目标 */
|
|
slotId: z.uuid().optional(),
|
|
});
|
|
export type LibraryVersionsQuery = z.infer<typeof LibraryVersionsQuery>;
|
|
|
|
export const LibraryVersions = z.object({
|
|
entityId: Id,
|
|
kind: MediaKind,
|
|
/** 参与比对的版本目标(当前最多一个);空 = 未配置 */
|
|
targets: z.array(z.object({ slotId: Id, name: z.string() })),
|
|
summary: VersionSummary,
|
|
seasons: z.array(SeasonVersions),
|
|
/** 电影:全部版本与比对 */
|
|
movie: z.object({ versions: z.array(AssetVersion), matches: z.array(UnitMatch).optional() }).nullable(),
|
|
/** 剧集里没对上具体某一集的视频文件 */
|
|
unplaced: z.array(AssetVersion),
|
|
});
|
|
export type LibraryVersions = z.infer<typeof LibraryVersions>;
|