202 lines
6.6 KiB
TypeScript
202 lines
6.6 KiB
TypeScript
import { z } from "zod";
|
|
import { Id, Timestamp } from "./common.ts";
|
|
|
|
// 补字幕(docs/plan/subtitles.md):上传 / 在线下载 → 解析(语言、季集)→ 配对到视频 → 写到视频同目录 → 历史可撤销。
|
|
|
|
export const SubtitleLanguage = z.enum([
|
|
"zh-Hans",
|
|
"zh-Hant",
|
|
"zh-Hans&en",
|
|
"zh-Hant&en",
|
|
"zh&ja",
|
|
"zh&ko",
|
|
"en",
|
|
"ja",
|
|
"ko",
|
|
"unknown",
|
|
]);
|
|
export type SubtitleLanguage = z.infer<typeof SubtitleLanguage>;
|
|
export const SubtitleExt = z.enum(["srt", "ass", "ssa", "vtt", "sub", "sbv"]);
|
|
export const SubtitleSource = z.enum(["upload", "subhd", "zimuku"]);
|
|
export type SubtitleSource = z.infer<typeof SubtitleSource>;
|
|
|
|
/** 可写字幕的视频(作品在媒体库里的视频位置) */
|
|
export const SubtitleTarget = z.object({
|
|
locationId: Id,
|
|
season: z.number().int().nullable(),
|
|
episode: z.number().int().nullable(),
|
|
fileName: z.string(),
|
|
relativePath: z.string(),
|
|
libraryRootName: z.string(),
|
|
/** 同目录已有的同名字幕 */
|
|
existing: z.array(z.object({ fileName: z.string(), language: SubtitleLanguage })),
|
|
});
|
|
export type SubtitleTarget = z.infer<typeof SubtitleTarget>;
|
|
|
|
export const SubtitleTargetsQuery = z.object({ entityId: Id });
|
|
export const SubtitleTargetsResponse = z.object({
|
|
entityId: Id,
|
|
kind: z.enum(["movie", "tv"]),
|
|
targets: z.array(SubtitleTarget),
|
|
});
|
|
export type SubtitleTargetsResponse = z.infer<typeof SubtitleTargetsResponse>;
|
|
|
|
export const SubtitleParseRequest = z.object({
|
|
/** 字幕文件或 zip;内容 base64 */
|
|
files: z
|
|
.array(z.object({ name: z.string().min(1).max(500), base64: z.string().max(40_000_000) }))
|
|
.min(1)
|
|
.max(200),
|
|
/** 给出时按这个作品的视频配对 */
|
|
entityId: Id.optional(),
|
|
});
|
|
export type SubtitleParseRequest = z.infer<typeof SubtitleParseRequest>;
|
|
|
|
export const SubtitleParsedItem = z.object({
|
|
id: z.string(),
|
|
/** 文件名(压缩包里的带目录) */
|
|
name: z.string(),
|
|
/** 来自哪个压缩包 */
|
|
archive: z.string().nullable(),
|
|
ext: SubtitleExt,
|
|
language: SubtitleLanguage,
|
|
season: z.number().int().nullable(),
|
|
seasonInferred: z.boolean(),
|
|
episode: z.number().int().nullable(),
|
|
encoding: z.string(),
|
|
sizeBytes: z.number().int(),
|
|
/** 已转成 UTF-8 的内容(base64),写入时原样提交 */
|
|
content: z.string(),
|
|
/** 建议的视频位置 */
|
|
targetLocationId: Id.nullable(),
|
|
/** 在线下载的:字幕站与条目地址(写入时记进历史) */
|
|
source: SubtitleSource.optional(),
|
|
sourceRef: z.string().optional(),
|
|
});
|
|
export type SubtitleParsedItem = z.infer<typeof SubtitleParsedItem>;
|
|
|
|
export const SubtitleParseResponse = z.object({
|
|
items: z.array(SubtitleParsedItem),
|
|
skipped: z.array(z.object({ name: z.string(), reason: z.string() })),
|
|
});
|
|
export type SubtitleParseResponse = z.infer<typeof SubtitleParseResponse>;
|
|
|
|
export const SubtitleApplyRequest = z.object({
|
|
entityId: Id,
|
|
source: SubtitleSource.default("upload"),
|
|
items: z
|
|
.array(
|
|
z.object({
|
|
name: z.string().min(1).max(500),
|
|
ext: SubtitleExt,
|
|
language: SubtitleLanguage,
|
|
content: z.string().max(40_000_000),
|
|
targetLocationId: Id,
|
|
/** 在线来源的条目地址 */
|
|
sourceRef: z.string().max(1000).optional(),
|
|
/** 这一项的来源(不给用整批的 source) */
|
|
source: SubtitleSource.optional(),
|
|
}),
|
|
)
|
|
.min(1)
|
|
.max(200),
|
|
});
|
|
export type SubtitleApplyRequest = z.input<typeof SubtitleApplyRequest>;
|
|
|
|
export const SubtitleWriteView = z.object({
|
|
id: Id,
|
|
mediaEntityId: Id,
|
|
videoLocationId: Id.nullable(),
|
|
fileName: z.string(),
|
|
relativePath: z.string(),
|
|
language: SubtitleLanguage,
|
|
source: SubtitleSource,
|
|
sourceName: z.string(),
|
|
/** 覆盖了同名字幕(撤销时还原) */
|
|
replaced: z.boolean(),
|
|
createdAt: Timestamp,
|
|
undoneAt: Timestamp.nullable(),
|
|
});
|
|
export type SubtitleWriteView = z.infer<typeof SubtitleWriteView>;
|
|
|
|
export const SubtitleApplyResult = z.object({
|
|
written: z.array(SubtitleWriteView),
|
|
failed: z.array(z.object({ name: z.string(), reason: z.string() })),
|
|
});
|
|
export type SubtitleApplyResult = z.infer<typeof SubtitleApplyResult>;
|
|
|
|
/** 表 subtitle_write 的行(0064) */
|
|
export const SubtitleWriteRow = z.object({
|
|
id: Id,
|
|
mediaEntityId: Id,
|
|
videoLocationId: Id.nullable(),
|
|
subtitleAssetId: Id.nullable(),
|
|
libraryRootId: Id,
|
|
relativePath: z.string(),
|
|
language: z.string(),
|
|
source: SubtitleSource,
|
|
sourceName: z.string(),
|
|
sourceRef: z.string().nullable(),
|
|
backupPath: z.string().nullable(),
|
|
actor: z.string().nullable(),
|
|
createdAt: Timestamp,
|
|
undoneAt: Timestamp.nullable(),
|
|
});
|
|
|
|
export const SubtitleWritesQuery = z.object({ entityId: Id });
|
|
export const SubtitleWritesResponse = z.object({ items: z.array(SubtitleWriteView) });
|
|
|
|
// ── 在线(字幕站:SubHD、字幕库) ──
|
|
|
|
export const SubtitleSearchRequest = z.object({
|
|
entityId: Id,
|
|
/** 剧集:只要这一季(集)的字幕;不给 = 全部 */
|
|
season: z.number().int().min(0).optional(),
|
|
episode: z.number().int().min(0).optional(),
|
|
/** 额外的搜索词(作品名搜不到时手动填) */
|
|
keyword: z.string().trim().max(100).optional(),
|
|
});
|
|
export type SubtitleSearchRequest = z.infer<typeof SubtitleSearchRequest>;
|
|
|
|
export const SubtitleSearchResult = z.object({
|
|
siteId: Id,
|
|
siteKey: z.string(),
|
|
siteName: z.string(),
|
|
/** 站内引用(下载时原样提交) */
|
|
ref: z.string(),
|
|
title: z.string(),
|
|
pageUrl: z.string(),
|
|
languages: z.array(z.string()),
|
|
format: z.string().nullable(),
|
|
downloads: z.number().int().nullable(),
|
|
uploader: z.string().nullable(),
|
|
publishedAt: z.string().nullable(),
|
|
season: z.number().int().nullable(),
|
|
episode: z.number().int().nullable(),
|
|
score: z.number(),
|
|
});
|
|
export type SubtitleSearchResult = z.infer<typeof SubtitleSearchResult>;
|
|
|
|
export const SubtitleSearchResponse = z.object({
|
|
results: z.array(SubtitleSearchResult),
|
|
/** 用到的搜索词 */
|
|
keywords: z.array(z.string()),
|
|
/** 各站出错 / 需要人工处理 */
|
|
errors: z.array(z.object({ siteName: z.string(), reason: z.string() })),
|
|
/** 没有启用的字幕站 */
|
|
noSites: z.boolean(),
|
|
});
|
|
export type SubtitleSearchResponse = z.infer<typeof SubtitleSearchResponse>;
|
|
|
|
/** 下载一条在线字幕:搜索结果给 siteId + ref;原站视图里人工打开的字幕页给 sessionId(取当前页与浏览器里的 Cookie) */
|
|
export const SubtitleFetchRequest = z
|
|
.object({
|
|
entityId: Id,
|
|
siteId: Id.optional(),
|
|
ref: z.string().min(1).max(500).optional(),
|
|
sessionId: Id.optional(),
|
|
title: z.string().max(500).default(""),
|
|
})
|
|
.refine((v) => !!v.sessionId || (!!v.siteId && !!v.ref), { message: "给 sessionId,或 siteId 加 ref" });
|
|
export type SubtitleFetchRequest = z.input<typeof SubtitleFetchRequest>;
|