import { z } from "zod"; import { Id, JsonObject, Timestamp } from "./common.ts"; // Independent reference shape avoids settings → instances → settings import cycles. const SourceRef = z.object({ root: z.string().regex(/^(lib:[0-9a-f-]{36}|dl:[0-9a-f-]{36}:\d{1,3})$/i), path: z .string() .max(4096) .refine((p) => !p.startsWith("/") && !p.includes("\0") && !p.split("/").some((s) => s === ".." || s === ".")), }); export const OrganizeSourceSnapshotRow = z.object({ sourceId: Id, snapshot: JsonObject, baselineAt: Timestamp.nullable(), lastScanAt: Timestamp.nullable(), lastError: z.string().nullable(), }); export type OrganizeSourceSnapshotRow = z.infer; export const OrganizeSourceInput = z.object({ name: z.string().trim().min(1).max(100), enabled: z.boolean(), source: SourceRef, target: SourceRef.refine((v) => v.root.startsWith("lib:"), { message: "目标必须为媒体库" }), mediaType: z.enum(["all", "movie", "tv"]), category: z .string() .trim() .min(1) .max(100) .refine( (s) => !/[\\/<>:"|?*]/.test(s) && !Array.from(s).some((ch) => ch.charCodeAt(0) < 32) && s !== "." && s !== "..", ) .nullable(), action: z.enum(["copy", "move"]), trigger: z.enum(["manual", "qb_tag", "snapshot"]), downloaderInstanceId: Id.nullable(), tag: z.string().trim().min(1).max(100).nullable(), intervalSec: z.number().int().min(60).max(86400), }); export type OrganizeSourceInput = z.infer; export const OrganizeSourceConfig = OrganizeSourceInput.extend({ id: Id }); export type OrganizeSourceConfig = z.infer; export const OrganizeSourceView = OrganizeSourceConfig.extend({ baselineAt: Timestamp.nullable(), lastScanAt: Timestamp.nullable(), lastError: z.string().nullable(), fileCount: z.number().int().nonnegative(), }); export type OrganizeSourceView = z.infer; export const OrganizeSourceList = z.object({ items: z.array(OrganizeSourceView) }); export const OrganizeStockInput = z.object({ path: SourceRef.shape.path.optional() }); export const OrganizeStockResult = z.object({ items: z.array(SourceRef), skipped: z.number().int().nonnegative(), unavailable: z.number().int().nonnegative(), truncated: z.boolean(), }); export type OrganizeStockResult = z.infer;