// Client-safe archive option types and defaults. The archive controller // (archiveTranscripts.ts) pulls in node:fs/promises and execa, which makes // it unsafe to import from "use client" files; the build-page UI needs the // type and the defaults, so they live here as a pure module. export type ArchiveFormat = "tar.gz" | "tar.xz" | "zip"; // `zip` first so it leads the editor's format dropdown, matching the default. export const ARCHIVE_FORMATS: ReadonlyArray = [ "zip", "tar.gz", "tar.xz", ]; export type ArchiveBuildOptions = { format: ArchiveFormat; // 1 (fastest, biggest) … 9 (slowest, smallest). Common to all three formats. compressionLevel: number; // When false, archive entries are stripped to { version, source, cues }. // Per-video addressability still comes from the file path. includeMetadata: boolean; // When true, cues files are written with 2-space indentation. Channel // manifests are always indented. prettyPrint: boolean; }; export const DEFAULT_ARCHIVE_OPTIONS: ArchiveBuildOptions = { format: "zip", compressionLevel: 6, includeMetadata: true, prettyPrint: false, }; // --- Served archive manifest ------------------------------------------------- // compose-site.ts writes public/archives/manifest.json describing the bulk // download zips it produced for a site. The Downloads page reads it to list the // archives with sizes/counts without hardcoding filenames. Client-safe (no fs). export type ArchiveManifestEntry = { kind: "transcripts" | "live-chat"; // "all" for the combined archive, otherwise a channel slug. scope: "all" | string; // Present when scope is a channel slug. channelTitle?: string; // Filename relative to /archives/ (e.g. "all-transcripts.zip", "grumps.zip"). filename: string; bytes: number; videoCount: number; // Set when the file exceeded thresholdBytes but was uploaded to overflow object // storage (Cloudflare R2) instead of being dropped: the absolute public URL to // download it from. When present, the UI links here and the entry is NOT // oversize-unavailable. Absent for archives served locally from /archives/. url?: string; // True when the file exceeded thresholdBytes AND no overflow storage was // configured: it is NOT served (removed so a size-capped host like Cloudflare // Pages won't reject the whole deploy) and the UI renders it as unavailable // rather than a broken link. Mutually exclusive with `url`. oversize?: boolean; }; export type ArchiveManifest = { version: 1; // The size cap applied this build, so the UI can explain omissions. 0 = no cap. thresholdBytes: number; entries: ArchiveManifestEntry[]; }; export const ARCHIVE_MANIFEST_FILENAME = "manifest.json"; // Default per-file size cap. Cloudflare Pages rejects any single asset > 25 MB, // which is why oversize combined archives must be dropped from what's served. export const DEFAULT_ARCHIVE_MAX_BYTES = 25 * 1024 * 1024;