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 JobTemplateProps {
14 /**
15 * Name of the job template. Must be unique within the account/region and
16 * match `^[\w-]+$`. If omitted, a unique name is generated. Changing the
17 * name replaces the job template.
18 */
19 jobTemplateName?: string;
20 /**
21 * Optional description of the job template.
22 */
23 description?: string;
24 /**
25 * Optional category used to group job templates in the MediaConvert console.
26 */
27 category?: string;
28 /**
29 * Name of the queue jobs created from this template are submitted to. If
30 * omitted, the account's `Default` queue is used.
31 */
32 queue?: string;
33 /**
34 * Relative priority (-50 to 50) of jobs created from this template.
35 */
36 priority?: number;
37 /**
38 * The transcode settings — input template(s), output groups, timecode
39 * config, and so on — applied to jobs created from this template. Required.
40 */
41 settings: mediaconvert.JobTemplateSettings;
42 /**
43 * Accelerated transcoding configuration.
44 */
45 accelerationSettings?: mediaconvert.AccelerationSettings;
46 /**
47 * Optional destination queues jobs hop to if they wait too long.
48 */
49 hopDestinations?: mediaconvert.HopDestination[];
50 /**
51 * How often, in seconds, MediaConvert emits `STATUS_UPDATE` events for jobs
52 * created from this template.
53 */
54 statusUpdateInterval?: mediaconvert.StatusUpdateInterval;
55 /**
56 * User-defined tags for the job template.
57 */
58 tags?: Record<string, string>;
59}
60
61export interface JobTemplate extends Resource<
62 "AWS.MediaConvert.JobTemplate",
63 JobTemplateProps,
64 {
65 jobTemplateName: string;
66 jobTemplateArn: string;
67 type: string | undefined;
68 category: string | undefined;
69 },
70 never,
71 Providers
72> {}
73
74/**
75 * An AWS Elemental MediaConvert job template — a reusable, named transcode
76 * configuration (inputs, output groups, and job-level settings) that new jobs
77 * are created from so callers only supply the input/output specifics.
78 *
79 * ### Creating a Job Template
80 * **Example:** MP4 File-Group Template
81 * ```typescript
82 * const template = yield* MediaConvert.JobTemplate("Mp4", {
83 * description: "Single MP4 output",
84 * settings: {
85 * Inputs: [{ TimecodeSource: "ZEROBASED" }],
86 * OutputGroups: [
87 * {
88 * OutputGroupSettings: {
89 * Type: "FILE_GROUP_SETTINGS",
90 * FileGroupSettings: {},
91 * },
92 * Outputs: [
93 * {
94 * ContainerSettings: { Container: "MP4" },
95 * VideoDescription: {
96 * CodecSettings: {
97 * Codec: "H_264",
98 * H264Settings: {
99 * RateControlMode: "QVBR",
100 * MaxBitrate: 5000000,
101 * },
102 * },
103 * },
104 * },
105 * ],
106 * },
107 * ],
108 * },
109 * });
110 * ```
111 *
112 * @resource
113 */
114export const JobTemplate = Resource<JobTemplate>(
115 "AWS.MediaConvert.JobTemplate",
116);
117
118export const JobTemplateProvider = () =>
119 Provider.effect(
120 JobTemplate,
121 Effect.gen(function* () {
122 const createName = Effect.fn(function* (
123 id: string,
124 props: JobTemplateProps,
125 ) {
126 return (
127 props.jobTemplateName ??
128 (yield* createPhysicalName({ id, maxLength: 128 }))
129 );
130 });
131
132 const toAttrs = (
133 template: mediaconvert.JobTemplate & { Name: string },
134 ) => ({
135 jobTemplateName: template.Name,
136 jobTemplateArn: template.Arn!,
137 type: template.Type,
138 category: template.Category,
139 });
140
141 /** Get a job template by name; typed not-found → undefined. */
142 const getJobTemplate = Effect.fn(function* (name: string) {
143 const response = yield* mediaconvert
144 .getJobTemplate({ Name: name })
145 .pipe(
146 Effect.catchTag("NotFoundException", () =>
147 Effect.succeed(undefined),
148 ),
149 );
150 return response?.JobTemplate;
151 });
152
153 return {
154 stables: ["jobTemplateName", "jobTemplateArn"],
155
156 diff: Effect.fn(function* ({ id, olds, news }) {
157 if (!isResolved(news)) return undefined;
158 const oldName = yield* createName(id, olds);
159 const newName = yield* createName(id, news);
160 if (oldName !== newName) return { action: "replace" } as const;
161 }),
162
163 read: Effect.fn(function* ({ id, olds, output }) {
164 const name =
165 output?.jobTemplateName ?? (yield* createName(id, olds ?? {}));
166 const template = yield* getJobTemplate(name);
167 if (template === undefined) return undefined;
168 const attrs = toAttrs(template);
169 const tags = yield* readMcTags(template.Arn!);
170 return (yield* hasAlchemyTags(id, tags)) ? attrs : Unowned(attrs);
171 }),
172
173 reconcile: Effect.fn(function* ({ id, news, output, session }) {
174 const internalTags = yield* createInternalTags(id);
175 const desiredTags = { ...internalTags, ...news.tags };
176 const name = output?.jobTemplateName ?? (yield* createName(id, news));
177
178 // 1. Observe — cloud state is authoritative.
179 let template = yield* getJobTemplate(name);
180
181 // 2. Ensure — create if missing.
182 if (template === undefined) {
183 const created = yield* mediaconvert.createJobTemplate({
184 Name: name,
185 Description: news.description,
186 Category: news.category,
187 Queue: news.queue,
188 Priority: news.priority,
189 Settings: news.settings,
190 AccelerationSettings: news.accelerationSettings,
191 HopDestinations: news.hopDestinations,
192 StatusUpdateInterval: news.statusUpdateInterval,
193 Tags: desiredTags,
194 });
195 template = created.JobTemplate!;
196 } else {
197 // 3. Sync — UpdateJobTemplate re-applies the mutable fields.
198 const updated = yield* mediaconvert.updateJobTemplate({
199 Name: name,
200 Description: news.description,
201 Category: news.category,
202 Queue: news.queue,
203 Priority: news.priority,
204 Settings: news.settings,
205 AccelerationSettings: news.accelerationSettings,
206 HopDestinations: news.hopDestinations,
207 StatusUpdateInterval: news.statusUpdateInterval,
208 });
209 template = updated.JobTemplate!;
210 }
211
212 // 3b. Sync tags — diff against OBSERVED cloud tags.
213 yield* syncMcTags(template.Arn!, desiredTags);
214
215 yield* session.note(name);
216 return toAttrs(
217 template as mediaconvert.JobTemplate & { Name: string },
218 );
219 }),
220
221 delete: Effect.fn(function* ({ output }) {
222 yield* mediaconvert
223 .deleteJobTemplate({ Name: output.jobTemplateName })
224 .pipe(Effect.catchTag("NotFoundException", () => Effect.void));
225 }),
226
227 list: () =>
228 mediaconvert.listJobTemplates.pages({}).pipe(
229 Stream.runCollect,
230 Effect.map((chunk) =>
231 Array.from(chunk).flatMap((page) => page.JobTemplates ?? []),
232 ),
233 Effect.map((templates) => templates.map(toAttrs)),
234 ),
235 };
236 }),
237 );