Files
UHD_Media_Tool/packages/contracts/src/library-versions.ts
T
rootandClaude Opus 5.5 71b129d85a feat(web): merge search filters into one row and tidy episode versions (POLISH-1)
- 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]>
2026-09-29 22:06:32 +08:00

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>;