1import * as mediaconvert from "@distilled.cloud/aws/mediaconvert";
2import * as Effect from "effect/Effect";
3import * as Stream from "effect/Stream";
4import { Unowned } from "../../AdoptPolicy.ts";
5import { isResolved } from "../../Diff.ts";
6import { createPhysicalName } from "../../PhysicalName.ts";
7import * as Provider from "../../Provider.ts";
8import { Resource } from "../../Resource.ts";
9import { createInternalTags, hasAlchemyTags } from "../../Tags.ts";
10import type { Providers } from "../Providers.ts";
11import { readMcTags, syncMcTags } from "./internal.ts";
12
13export interface PresetProps {
14 /**
15 * Name of the preset. Must be unique within the account/region and match
16 * `^[\w-]+$`. If omitted, a unique name is generated. Changing the name
17 * replaces the preset.
18 */
19 presetName?: string;
20 /**
21 * Optional description of the preset.
22 */
23 description?: string;
24 /**
25 * Optional category used to group presets in the MediaConvert console.
26 */
27 category?: string;
28 /**
29 * The transcode output settings this preset applies — audio descriptions,
30 * caption descriptions, container settings, and the video description
31 * (codec, resolution, bitrate, …). Required.
32 */
33 settings: mediaconvert.PresetSettings;
34 /**
35 * User-defined tags for the preset.
36 */
37 tags?: Record<string, string>;
38}
39
40export interface Preset extends Resource<
41 "AWS.MediaConvert.Preset",
42 PresetProps,
43 {
44 presetName: string;
45 presetArn: string;
46 type: string | undefined;
47 category: string | undefined;
48 },
49 never,
50 Providers
51> {}
52
53/**
54 * An AWS Elemental MediaConvert output preset — a reusable, named bundle of
55 * output settings (container, video codec/resolution/bitrate, audio, and
56 * captions) that job templates and jobs reference to produce one output.
57 *
58 * ### Creating a Preset
59 * **Example:** MP4 / H.264 Preset
60 * ```typescript
61 * const preset = yield* MediaConvert.Preset("Mp4", {
62 * description: "1080p H.264 MP4",
63 * settings: {
64 * ContainerSettings: { Container: "MP4" },
65 * VideoDescription: {
66 * Width: 1920,
67 * Height: 1080,
68 * CodecSettings: {
69 * Codec: "H_264",
70 * H264Settings: { RateControlMode: "QVBR", MaxBitrate: 5000000 },
71 * },
72 * },
73 * AudioDescriptions: [
74 * {
75 * CodecSettings: {
76 * Codec: "AAC",
77 * AacSettings: {
78 * Bitrate: 96000,
79 * CodingMode: "CODING_MODE_2_0",
80 * SampleRate: 48000,
81 * },
82 * },
83 * },
84 * ],
85 * },
86 * });
87 * ```
88 *
89 * @resource
90 */
91export const Preset = Resource<Preset>("AWS.MediaConvert.Preset");
92
93export const PresetProvider = () =>
94 Provider.effect(
95 Preset,
96 Effect.gen(function* () {
97 const createName = Effect.fn(function* (id: string, props: PresetProps) {
98 return (
99 props.presetName ??
100 (yield* createPhysicalName({ id, maxLength: 128 }))
101 );
102 });
103
104 const toAttrs = (preset: mediaconvert.Preset & { Name: string }) => ({
105 presetName: preset.Name,
106 presetArn: preset.Arn!,
107 type: preset.Type,
108 category: preset.Category,
109 });
110
111 /** Get a preset by name; typed not-found → undefined. */
112 const getPreset = Effect.fn(function* (name: string) {
113 const response = yield* mediaconvert
114 .getPreset({ Name: name })
115 .pipe(
116 Effect.catchTag("NotFoundException", () =>
117 Effect.succeed(undefined),
118 ),
119 );
120 return response?.Preset;
121 });
122
123 return {
124 stables: ["presetName", "presetArn"],
125
126 diff: Effect.fn(function* ({ id, olds, news }) {
127 if (!isResolved(news)) return undefined;
128 const oldName = yield* createName(id, olds);
129 const newName = yield* createName(id, news);
130 if (oldName !== newName) return { action: "replace" } as const;
131 }),
132
133 read: Effect.fn(function* ({ id, olds, output }) {
134 const name =
135 output?.presetName ?? (yield* createName(id, olds ?? {}));
136 const preset = yield* getPreset(name);
137 if (preset === undefined) return undefined;
138 const attrs = toAttrs(preset);
139 const tags = yield* readMcTags(preset.Arn!);
140 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
141 }),
142
143 reconcile: Effect.fn(function* ({ id, news, output, session }) {
144 const internalTags = yield* createInternalTags(id);
145 const desiredTags = { ...internalTags, ...news.tags };
146 const name = output?.presetName ?? (yield* createName(id, news));
147
148 // 1. Observe — cloud state is authoritative.
149 let preset = yield* getPreset(name);
150
151 // 2. Ensure — create if missing.
152 if (preset === undefined) {
153 const created = yield* mediaconvert.createPreset({
154 Name: name,
155 Description: news.description,
156 Category: news.category,
157 Settings: news.settings,
158 Tags: desiredTags,
159 });
160 preset = created.Preset!;
161 } else {
162 // 3. Sync — UpdatePreset re-applies the mutable fields in place.
163 const updated = yield* mediaconvert.updatePreset({
164 Name: name,
165 Description: news.description,
166 Category: news.category,
167 Settings: news.settings,
168 });
169 preset = updated.Preset!;
170 }
171
172 // 3b. Sync tags — diff against OBSERVED cloud tags.
173 yield* syncMcTags(preset.Arn!, desiredTags);
174
175 yield* session.note(name);
176 return toAttrs(preset as mediaconvert.Preset & { Name: string });
177 }),
178
179 delete: Effect.fn(function* ({ output }) {
180 yield* mediaconvert
181 .deletePreset({ Name: output.presetName })
182 .pipe(Effect.catchTag("NotFoundException", () => Effect.void));
183 }),
184
185 list: () =>
186 mediaconvert.listPresets.pages({}).pipe(
187 Stream.runCollect,
188 Effect.map((chunk) =>
189 Array.from(chunk).flatMap((page) => page.Presets ?? []),
190 ),
191 Effect.map((presets) => presets.map(toAttrs)),
192 ),
193 };
194 }),
195 );