feat(subtitles): 补字幕第 1 步——上传字幕 / zip,识别语言与季集,配对写入视频同目录,历史与撤销
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
feee481915
commit
ef29bd62ce
22 files changed
+1554
-2
No files matched your search
@@ -0,0 +1,124 @@
|
||||
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(),
|
||||
});
|
||||
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(),
|
||||
}),
|
||||
)
|
||||
.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>;
|
||||
|
||||
export const SubtitleWritesQuery = z.object({ entityId: Id });
|
||||
export const SubtitleWritesResponse = z.object({ items: z.array(SubtitleWriteView) });
|
||||
Reference in new issue
Block a user