tracksmith 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -8
- package/dist/tracksmith.js +355 -84
- package/dist/tracksmith.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
# tracksmith
|
|
2
2
|
|
|
3
|
-
Tracksmith is a CLI for MKV files. It inspects tracks, extracts an audio track losslessly, creates short clips for checking audio/video sync, and muxes aligned audio into another MKV. It wraps FFmpeg and MKVToolNix. Audio tracks are always stream-copied, so Tracksmith never transcodes or re-encodes audio.
|
|
3
|
+
Tracksmith is a CLI for MKV files. It inspects tracks, edits metadata in place, extracts an audio track losslessly, creates short clips for checking audio/video sync, and muxes aligned audio into another MKV. It wraps FFmpeg and MKVToolNix. Audio tracks are always stream-copied, so Tracksmith never transcodes or re-encodes audio.
|
|
4
4
|
|
|
5
5
|
## Requirements
|
|
6
6
|
|
|
7
7
|
- [Node.js](https://nodejs.org) 22 or newer, or [Bun](https://bun.com) 1.3.14 or newer
|
|
8
|
-
- `ffmpeg` and `
|
|
8
|
+
- `ffmpeg`, `mkvmerge`, and `mkvpropedit` available on `PATH`
|
|
9
9
|
|
|
10
10
|
`ffmpeg`:
|
|
11
11
|
|
|
@@ -24,7 +24,7 @@ sudo apt install ffmpeg
|
|
|
24
24
|
sudo pacman -S ffmpeg
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
`mkvmerge
|
|
27
|
+
`mkvmerge` and `mkvpropedit` (both part of MKVToolNix):
|
|
28
28
|
|
|
29
29
|
```powershell
|
|
30
30
|
# Windows (choose one)
|
|
@@ -66,11 +66,15 @@ Release process and approval boundaries: [docs/releasing.md](docs/releasing.md).
|
|
|
66
66
|
```powershell
|
|
67
67
|
tracksmith inspect <file>
|
|
68
68
|
tracksmith extract <file> --track <id> [--output <file>] [--force]
|
|
69
|
+
tracksmith edit <file> --track <id> [--name <text>] [--language <lang>] [--default | --no-default] [--forced | --no-forced]
|
|
70
|
+
tracksmith edit <file> --title <text>
|
|
69
71
|
tracksmith test-clip --video <target> --audio <donor> --track <id> --start <time> [--duration 60] [--delay-ms 0] [--output <file>] [--force]
|
|
70
|
-
tracksmith mux --video <target> --audio <mka-or-mkv>
|
|
72
|
+
tracksmith mux --video <target> --output <file> --audio <mka-or-mkv> [--track <id>] [--delay-ms 0] [--language eng] [--name <title>] [--default] [--audio <next> ...] [--dry-run] [--force]
|
|
71
73
|
```
|
|
72
74
|
|
|
73
|
-
`--track` is always the MKVToolNix track ID shown by `tracksmith inspect`. Times accept seconds such as `90` or `90.5`, or `HH:MM:SS[.ms]`.
|
|
75
|
+
`--track` is always the MKVToolNix track ID shown by `tracksmith inspect`. For `mux`, repeat the full `--audio` group for each new track; its per-track flags apply to the most recent `--audio`, and `--default` may be used on at most one group. Using `--default` also clears the default flag on the target's existing audio tracks, so the new track really becomes the default. Times accept seconds such as `90` or `90.5`, or `HH:MM:SS[.ms]`.
|
|
76
|
+
|
|
77
|
+
`edit` changes metadata in place without remuxing. `--name ""` and `--title ""` clear the value, and `--title` (the file title) cannot be combined with `--track` edits. In Windows PowerShell, use the equals forms `--name=` and `--title=` to clear — PowerShell drops empty `""` arguments before they reach the CLI.
|
|
74
78
|
|
|
75
79
|
## PowerShell walkthrough
|
|
76
80
|
|
|
@@ -84,13 +88,16 @@ tracksmith extract "D:\rips\Movie DonorCut.mkv" --track 2
|
|
|
84
88
|
# 3. Make a 60-second sync clip at the 10-minute mark, audio delayed 250 ms
|
|
85
89
|
tracksmith test-clip --video "D:\rips\Movie TargetCut.mkv" --audio "D:\rips\Movie DonorCut.track2.mka" --track 0 --start 00:10:00 --delay-ms 250
|
|
86
90
|
|
|
87
|
-
# 4.
|
|
88
|
-
tracksmith mux --video "D:\rips\Movie TargetCut.mkv" --audio "D:\rips\Movie
|
|
91
|
+
# 4. Preview two tracks from the same donor without writing an output file
|
|
92
|
+
tracksmith mux --video "D:\rips\Movie TargetCut.mkv" --audio "D:\rips\Movie DonorTracks.mka" --track 0 --delay-ms 250 --language eng --name "Original E-AC-3 7.1" --default --audio "D:\rips\Movie DonorTracks.mka" --track 1 --delay-ms 0 --language eng --name "Commentary" --output "D:\rips\Movie Final.mkv" --dry-run
|
|
93
|
+
|
|
94
|
+
# 5. Once the plan looks right, mux both tracks into the target
|
|
95
|
+
tracksmith mux --video "D:\rips\Movie TargetCut.mkv" --audio "D:\rips\Movie DonorTracks.mka" --track 0 --delay-ms 250 --language eng --name "Original E-AC-3 7.1" --default --audio "D:\rips\Movie DonorTracks.mka" --track 1 --delay-ms 0 --language eng --name "Commentary" --output "D:\rips\Movie Final.mkv"
|
|
89
96
|
```
|
|
90
97
|
|
|
91
98
|
Positive `--delay-ms` delays audio; negative values advance it (e.g. `--delay-ms -250` or `--delay-ms=-250`). Check sync near the beginning, middle, and end of the file. If the required offset changes, the files differ by frame rate or edits and one constant delay cannot fix them.
|
|
92
99
|
|
|
93
|
-
|
|
100
|
+
For `mux`, an interactive terminal shows the planned output track table and asks one combined y/N confirmation; if the output already exists, that same confirmation also covers overwriting it. Use `--dry-run` to print the plan, listing every track already in the target plus the new tracks with `SOURCE` and `DELAY` columns, then exit without writing. Pass `--force` to skip all mux prompting; non-interactive mux sessions never prompt and still require `--force` to overwrite an existing output. `extract` and `test-clip` keep the old overwrite-only prompt behavior.
|
|
94
101
|
|
|
95
102
|
## Local development
|
|
96
103
|
|
package/dist/tracksmith.js
CHANGED
|
@@ -38,8 +38,12 @@ function sameFile(left, right) {
|
|
|
38
38
|
return false;
|
|
39
39
|
}
|
|
40
40
|
}
|
|
41
|
+
/** Returns the input path the output aliases on disk (same dev/inode), if any. */
|
|
42
|
+
function findAliasedInput(output, inputs) {
|
|
43
|
+
return inputs.find((input) => sameFile(output, input));
|
|
44
|
+
}
|
|
41
45
|
async function ensureWritable(path, ctx) {
|
|
42
|
-
const aliasedInput = ctx.inputs
|
|
46
|
+
const aliasedInput = findAliasedInput(path, ctx.inputs ?? []);
|
|
43
47
|
if (aliasedInput !== void 0) throw new CliError(`Output "${path}" is the same file as input "${aliasedInput}". Choose a different output path.`);
|
|
44
48
|
if (!(ctx.exists ?? existsSync)(path)) return;
|
|
45
49
|
if (ctx.force) return;
|
|
@@ -57,9 +61,11 @@ function toolPath(path) {
|
|
|
57
61
|
//#region src/probe.ts
|
|
58
62
|
const mkvmergePropertiesSchema = z.object({
|
|
59
63
|
language: z.optional(z.string()),
|
|
64
|
+
language_ietf: z.optional(z.string()),
|
|
60
65
|
track_name: z.optional(z.string()),
|
|
61
66
|
audio_channels: z.optional(z.number()),
|
|
62
|
-
default_track: z.optional(z.boolean())
|
|
67
|
+
default_track: z.optional(z.boolean()),
|
|
68
|
+
forced_track: z.optional(z.boolean())
|
|
63
69
|
});
|
|
64
70
|
const mkvmergeTrackSchema = z.object({
|
|
65
71
|
id: z.int(),
|
|
@@ -67,8 +73,16 @@ const mkvmergeTrackSchema = z.object({
|
|
|
67
73
|
codec: z.string(),
|
|
68
74
|
properties: z.optional(mkvmergePropertiesSchema)
|
|
69
75
|
});
|
|
70
|
-
const
|
|
71
|
-
|
|
76
|
+
const mkvmergeContainerSchema = z.object({ properties: z.optional(z.object({ title: z.optional(z.string()) })) });
|
|
77
|
+
const mkvmergeOutputSchema = z.object({
|
|
78
|
+
container: z.optional(z.unknown()),
|
|
79
|
+
tracks: z.array(mkvmergeTrackSchema)
|
|
80
|
+
});
|
|
81
|
+
function containerTitle(container) {
|
|
82
|
+
const result = z.safeParse(mkvmergeContainerSchema, container);
|
|
83
|
+
return result.success ? result.data.properties?.title : void 0;
|
|
84
|
+
}
|
|
85
|
+
function parseMkvmergeFile(json) {
|
|
72
86
|
let parsed;
|
|
73
87
|
try {
|
|
74
88
|
parsed = JSON.parse(json);
|
|
@@ -77,24 +91,31 @@ function parseMkvmergeJson(json) {
|
|
|
77
91
|
}
|
|
78
92
|
const result = z.safeParse(mkvmergeOutputSchema, parsed);
|
|
79
93
|
if (!result.success) throw new CliError("mkvmerge -J output has an unexpected structure.");
|
|
80
|
-
return
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
94
|
+
return {
|
|
95
|
+
title: containerTitle(result.data.container),
|
|
96
|
+
tracks: result.data.tracks.map((track) => ({
|
|
97
|
+
id: track.id,
|
|
98
|
+
type: track.type,
|
|
99
|
+
codec: track.codec,
|
|
100
|
+
language: track.properties?.language_ietf || track.properties?.language || void 0,
|
|
101
|
+
name: track.properties?.track_name,
|
|
102
|
+
channels: track.properties?.audio_channels,
|
|
103
|
+
isDefault: track.properties?.default_track ?? false,
|
|
104
|
+
isForced: track.properties?.forced_track ?? false
|
|
105
|
+
}))
|
|
106
|
+
};
|
|
89
107
|
}
|
|
90
|
-
async function
|
|
108
|
+
async function probeFile(runner, file) {
|
|
91
109
|
const result = await runner.run([
|
|
92
110
|
"mkvmerge",
|
|
93
111
|
"-J",
|
|
94
112
|
toolPath(file)
|
|
95
113
|
]);
|
|
96
114
|
if (result.exitCode >= 2) throw new CliError(`mkvmerge could not read "${file}" (exit ${result.exitCode}):\n${result.stderr || result.stdout}`);
|
|
97
|
-
return
|
|
115
|
+
return parseMkvmergeFile(result.stdout);
|
|
116
|
+
}
|
|
117
|
+
async function probeTracks(runner, file) {
|
|
118
|
+
return (await probeFile(runner, file)).tracks;
|
|
98
119
|
}
|
|
99
120
|
function validAudioIds(tracks) {
|
|
100
121
|
const ids = tracks.filter((track) => track.type === "audio").map((track) => track.id);
|
|
@@ -111,6 +132,54 @@ function audioRelativeIndex(tracks, id) {
|
|
|
111
132
|
if (index === -1) throw new CliError(`Track ${id} is not an audio track. ${validAudioIds(tracks)}`);
|
|
112
133
|
return index;
|
|
113
134
|
}
|
|
135
|
+
function requireTrack(tracks, id) {
|
|
136
|
+
const track = tracks.find((candidate) => candidate.id === id);
|
|
137
|
+
if (!track) {
|
|
138
|
+
const ids = tracks.map((candidate) => candidate.id);
|
|
139
|
+
throw new CliError(`Track ${id} does not exist. Valid track IDs: ${ids.length > 0 ? ids.join(", ") : "none"}`);
|
|
140
|
+
}
|
|
141
|
+
return track;
|
|
142
|
+
}
|
|
143
|
+
//#endregion
|
|
144
|
+
//#region src/commands/edit.ts
|
|
145
|
+
function hasTrackEdit(opts) {
|
|
146
|
+
return opts.name !== void 0 || opts.language !== void 0 || opts.isDefault !== void 0 || opts.isForced !== void 0;
|
|
147
|
+
}
|
|
148
|
+
function buildEditArgs(opts) {
|
|
149
|
+
const args = [
|
|
150
|
+
"mkvpropedit",
|
|
151
|
+
"--command-line-charset",
|
|
152
|
+
"UTF-8",
|
|
153
|
+
toolPath(opts.file)
|
|
154
|
+
];
|
|
155
|
+
switch (opts.kind) {
|
|
156
|
+
case "title":
|
|
157
|
+
args.push("--edit", "info");
|
|
158
|
+
if (opts.title === "") args.push("--delete", "title");
|
|
159
|
+
else args.push("--set", `title=${opts.title}`);
|
|
160
|
+
return args;
|
|
161
|
+
case "track":
|
|
162
|
+
args.push("--edit", `track:${opts.track + 1}`);
|
|
163
|
+
if (opts.name === "") args.push("--delete", "name");
|
|
164
|
+
else if (opts.name !== void 0) args.push("--set", `name=${opts.name}`);
|
|
165
|
+
if (opts.language !== void 0) args.push("--set", `language=${opts.language}`);
|
|
166
|
+
if (opts.isDefault !== void 0) args.push("--set", `flag-default=${opts.isDefault ? "1" : "0"}`);
|
|
167
|
+
if (opts.isForced !== void 0) args.push("--set", `flag-forced=${opts.isForced ? "1" : "0"}`);
|
|
168
|
+
return args;
|
|
169
|
+
default: return opts;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
/** Returns mkvpropedit's diagnostic output when it exited 1 (modified with warnings). */
|
|
173
|
+
async function editCommand(opts, deps) {
|
|
174
|
+
requireInputFile(opts.file, deps.exists);
|
|
175
|
+
if (opts.kind === "track") {
|
|
176
|
+
if (!hasTrackEdit(opts)) throw new CliError("Nothing to edit: pass --name, --language, --default/--no-default, or --forced/--no-forced.");
|
|
177
|
+
requireTrack(await probeTracks(deps.runner, opts.file), opts.track);
|
|
178
|
+
}
|
|
179
|
+
const result = await deps.runner.run(buildEditArgs(opts));
|
|
180
|
+
if (result.exitCode >= 2) throw new CliError(`mkvpropedit failed (exit ${result.exitCode}):\n${result.stderr || result.stdout}`);
|
|
181
|
+
if (result.exitCode === 1) return `mkvpropedit finished with warnings — check the file in a player:\n${[result.stdout, result.stderr].map((stream) => stream.trim()).filter(Boolean).join("\n")}`;
|
|
182
|
+
}
|
|
114
183
|
//#endregion
|
|
115
184
|
//#region src/commands/extract.ts
|
|
116
185
|
function buildExtractArgs(opts) {
|
|
@@ -150,34 +219,44 @@ async function extractCommand(opts, deps) {
|
|
|
150
219
|
}
|
|
151
220
|
//#endregion
|
|
152
221
|
//#region src/commands/inspect.ts
|
|
222
|
+
function formatTable(header, rows) {
|
|
223
|
+
const all = [header, ...rows];
|
|
224
|
+
const widths = header.map((_, column) => Math.max(...all.map((row) => row[column].length)));
|
|
225
|
+
return all.map((row) => row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd()).join("\n");
|
|
226
|
+
}
|
|
153
227
|
function formatTrackTable(tracks) {
|
|
154
|
-
|
|
228
|
+
return formatTable([
|
|
155
229
|
"ID",
|
|
156
230
|
"TYPE",
|
|
157
231
|
"CODEC",
|
|
158
232
|
"LANG",
|
|
159
233
|
"CH",
|
|
160
234
|
"DEFAULT",
|
|
235
|
+
"FORCED",
|
|
161
236
|
"NAME"
|
|
162
|
-
]
|
|
163
|
-
const all = [header, ...tracks.map((track) => [
|
|
237
|
+
], tracks.map((track) => [
|
|
164
238
|
String(track.id),
|
|
165
239
|
track.type,
|
|
166
240
|
track.codec,
|
|
167
241
|
track.language ?? "-",
|
|
168
242
|
track.channels !== void 0 ? String(track.channels) : "-",
|
|
169
243
|
track.isDefault ? "yes" : "-",
|
|
244
|
+
track.isForced ? "yes" : "-",
|
|
170
245
|
track.name ?? "-"
|
|
171
|
-
])
|
|
172
|
-
const widths = header.map((_, column) => Math.max(...all.map((row) => row[column].length)));
|
|
173
|
-
return all.map((row) => row.map((cell, column) => cell.padEnd(widths[column])).join(" ").trimEnd()).join("\n");
|
|
246
|
+
]));
|
|
174
247
|
}
|
|
175
248
|
async function inspectCommand(file, deps) {
|
|
176
249
|
requireInputFile(file, deps.exists);
|
|
177
|
-
|
|
250
|
+
const { title, tracks } = await probeFile(deps.runner, file);
|
|
251
|
+
const table = formatTrackTable(tracks);
|
|
252
|
+
return title === void 0 ? table : `Title: ${title}\n\n${table}`;
|
|
178
253
|
}
|
|
179
254
|
//#endregion
|
|
180
255
|
//#region src/commands/mux.ts
|
|
256
|
+
/** Labels one `--audio` group by position and path, e.g. `--audio #2 ("donor.mka")`. */
|
|
257
|
+
function describeAudioGroup(index, audio) {
|
|
258
|
+
return `--audio #${index + 1} ("${audio}")`;
|
|
259
|
+
}
|
|
181
260
|
function resolveAudioTrackId(tracks, requested) {
|
|
182
261
|
if (requested !== void 0) return requireAudioTrack(tracks, requested).id;
|
|
183
262
|
const audio = tracks.filter((track) => track.type === "audio");
|
|
@@ -186,51 +265,127 @@ function resolveAudioTrackId(tracks, requested) {
|
|
|
186
265
|
throw new CliError(`The audio input contains ${audio.length} audio tracks; pass --track <id> (see: tracksmith inspect <file>).`);
|
|
187
266
|
}
|
|
188
267
|
function buildMuxArgs(a) {
|
|
189
|
-
const
|
|
268
|
+
const trackOrder = [...a.videoTrackIds.map((trackId) => `0:${trackId}`), ...a.groups.map((group, index) => `${index + 1}:${group.trackId}`)].join(",");
|
|
190
269
|
const args = [
|
|
191
270
|
"mkvmerge",
|
|
271
|
+
"--command-line-charset",
|
|
272
|
+
"UTF-8",
|
|
192
273
|
"-o",
|
|
193
274
|
toolPath(a.output),
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
id,
|
|
197
|
-
"--no-video",
|
|
198
|
-
"--no-subtitles",
|
|
199
|
-
"--no-buttons",
|
|
200
|
-
"--no-chapters",
|
|
201
|
-
"--no-attachments",
|
|
202
|
-
"--no-global-tags"
|
|
275
|
+
"--track-order",
|
|
276
|
+
trackOrder
|
|
203
277
|
];
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
278
|
+
for (const id of a.clearDefaultAudioIds) args.push("--default-track-flag", `${id}:no`);
|
|
279
|
+
args.push(toolPath(a.video));
|
|
280
|
+
for (const group of a.groups) {
|
|
281
|
+
const id = String(group.trackId);
|
|
282
|
+
args.push("--audio-tracks", id, "--no-video", "--no-subtitles", "--no-buttons", "--no-chapters", "--no-attachments", "--no-global-tags");
|
|
283
|
+
if (group.delayMs !== 0) args.push("--sync", `${id}:${group.delayMs}`);
|
|
284
|
+
if (group.language !== void 0) args.push("--language", `${id}:${group.language}`);
|
|
285
|
+
if (group.trackName !== void 0) args.push("--track-name", `${id}:${group.trackName}`);
|
|
286
|
+
args.push("--default-track-flag", `${id}:${group.makeDefault ? "yes" : "no"}`);
|
|
287
|
+
args.push(toolPath(group.audio));
|
|
288
|
+
}
|
|
209
289
|
return args;
|
|
210
290
|
}
|
|
291
|
+
function buildPlan(a) {
|
|
292
|
+
const cleared = new Set(a.clearDefaultAudioIds);
|
|
293
|
+
const existingRows = a.videoTracks.map((track) => [
|
|
294
|
+
String(track.id),
|
|
295
|
+
track.type,
|
|
296
|
+
track.codec,
|
|
297
|
+
track.language ?? "-",
|
|
298
|
+
track.channels !== void 0 ? String(track.channels) : "-",
|
|
299
|
+
track.isDefault && !cleared.has(track.id) ? "yes" : "-",
|
|
300
|
+
"-",
|
|
301
|
+
track.name ?? "-",
|
|
302
|
+
a.video
|
|
303
|
+
]);
|
|
304
|
+
const newRows = a.groups.map((group, index) => {
|
|
305
|
+
const donor = a.sourceTracks.get(group.audio)?.find((track) => track.id === group.trackId);
|
|
306
|
+
return [
|
|
307
|
+
String(a.videoTracks.length + index),
|
|
308
|
+
"audio",
|
|
309
|
+
donor?.codec ?? "-",
|
|
310
|
+
group.language ?? donor?.language ?? "-",
|
|
311
|
+
donor?.channels !== void 0 ? String(donor.channels) : "-",
|
|
312
|
+
group.makeDefault ? "yes" : "-",
|
|
313
|
+
group.delayMs !== 0 ? `${group.delayMs}ms` : "-",
|
|
314
|
+
group.trackName ?? donor?.name ?? "-",
|
|
315
|
+
group.audio
|
|
316
|
+
];
|
|
317
|
+
});
|
|
318
|
+
const table = formatTable([
|
|
319
|
+
"ID",
|
|
320
|
+
"TYPE",
|
|
321
|
+
"CODEC",
|
|
322
|
+
"LANG",
|
|
323
|
+
"CH",
|
|
324
|
+
"DEFAULT",
|
|
325
|
+
"DELAY",
|
|
326
|
+
"NAME",
|
|
327
|
+
"SOURCE"
|
|
328
|
+
], [...existingRows, ...newRows]);
|
|
329
|
+
return `Planned tracks for "${a.output}":\n${table}`;
|
|
330
|
+
}
|
|
211
331
|
async function muxCommand(opts, deps) {
|
|
212
332
|
requireInputFile(opts.video, deps.exists);
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
await
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
333
|
+
const uniqueAudio = [...new Set(opts.tracks.map((track) => track.audio))];
|
|
334
|
+
for (const audio of uniqueAudio) requireInputFile(audio, deps.exists);
|
|
335
|
+
const videoTracks = await probeTracks(deps.runner, opts.video);
|
|
336
|
+
const sourceTracks = new Map(await Promise.all(uniqueAudio.map(async (audio) => [audio, await probeTracks(deps.runner, audio)])));
|
|
337
|
+
const groups = opts.tracks.map((track, index) => {
|
|
338
|
+
let trackId;
|
|
339
|
+
try {
|
|
340
|
+
trackId = resolveAudioTrackId(sourceTracks.get(track.audio), track.track);
|
|
341
|
+
} catch (error) {
|
|
342
|
+
if (!(error instanceof CliError)) throw error;
|
|
343
|
+
throw new CliError(`${describeAudioGroup(index, track.audio)}: ${error.message}`);
|
|
344
|
+
}
|
|
345
|
+
return {
|
|
346
|
+
audio: track.audio,
|
|
347
|
+
trackId,
|
|
348
|
+
delayMs: track.delayMs,
|
|
349
|
+
language: track.language,
|
|
350
|
+
trackName: track.name,
|
|
351
|
+
makeDefault: track.makeDefault
|
|
352
|
+
};
|
|
353
|
+
});
|
|
354
|
+
if (groups.filter((group) => group.makeDefault).length > 1) throw new CliError("--default can be set on at most one --audio input.");
|
|
355
|
+
const aliasedInput = findAliasedInput(opts.output, [opts.video, ...uniqueAudio]);
|
|
356
|
+
if (aliasedInput !== void 0) throw new CliError(`Output "${opts.output}" is the same file as input "${aliasedInput}". Choose a different output path.`);
|
|
357
|
+
const clearDefaultAudioIds = groups.some((group) => group.makeDefault) ? videoTracks.filter((track) => track.type === "audio" && track.isDefault).map((track) => track.id) : [];
|
|
358
|
+
const plan = buildPlan({
|
|
359
|
+
video: opts.video,
|
|
360
|
+
videoTracks,
|
|
361
|
+
groups,
|
|
362
|
+
sourceTracks,
|
|
363
|
+
clearDefaultAudioIds,
|
|
364
|
+
output: opts.output
|
|
221
365
|
});
|
|
366
|
+
if (opts.dryRun) return {
|
|
367
|
+
kind: "dry-run",
|
|
368
|
+
plan
|
|
369
|
+
};
|
|
370
|
+
const outputExists = (deps.exists ?? existsSync)(opts.output);
|
|
371
|
+
if (!opts.force) {
|
|
372
|
+
if (deps.isTTY) {
|
|
373
|
+
const overwrite = outputExists ? `\nOutput "${opts.output}" already exists and will be overwritten.` : "";
|
|
374
|
+
if (!await deps.confirm(`${plan}${overwrite}\nWrite "${opts.output}"?`)) throw new CliError("Aborted: nothing written.");
|
|
375
|
+
} else if (outputExists) throw new CliError(`Output "${opts.output}" already exists. Pass --force to overwrite (cannot prompt in a non-interactive session).`);
|
|
376
|
+
}
|
|
222
377
|
const result = await deps.runner.run(buildMuxArgs({
|
|
223
378
|
video: opts.video,
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
language: opts.language,
|
|
228
|
-
trackName: opts.name,
|
|
229
|
-
makeDefault: opts.makeDefault,
|
|
379
|
+
videoTrackIds: videoTracks.map((track) => track.id),
|
|
380
|
+
clearDefaultAudioIds,
|
|
381
|
+
groups,
|
|
230
382
|
output: opts.output
|
|
231
383
|
}), deps.stderrIsTTY ? { stream: "stdout" } : void 0);
|
|
232
384
|
if (result.exitCode >= 2) throw new CliError(`mkvmerge failed (exit ${result.exitCode}):\n${result.stderr || result.stdout}`);
|
|
233
|
-
return
|
|
385
|
+
return {
|
|
386
|
+
kind: "written",
|
|
387
|
+
output: opts.output
|
|
388
|
+
};
|
|
234
389
|
}
|
|
235
390
|
//#endregion
|
|
236
391
|
//#region src/commands/test-clip.ts
|
|
@@ -306,23 +461,32 @@ async function testClipCommand(opts, deps) {
|
|
|
306
461
|
}
|
|
307
462
|
//#endregion
|
|
308
463
|
//#region src/cli.ts
|
|
309
|
-
const USAGE = `tracksmith — inspect, extract, sync-test, and mux Matroska audio tracks
|
|
464
|
+
const USAGE = `tracksmith — inspect, edit, extract, sync-test, and mux Matroska audio tracks
|
|
310
465
|
|
|
311
466
|
Usage:
|
|
312
467
|
tracksmith inspect <file>
|
|
313
468
|
tracksmith extract <file> --track <id> [--output <file>] [--force]
|
|
469
|
+
tracksmith edit <file> --track <id> [--name <text>] [--language <lang>]
|
|
470
|
+
[--default | --no-default] [--forced | --no-forced]
|
|
471
|
+
tracksmith edit <file> --title <text>
|
|
314
472
|
tracksmith test-clip --video <target> --audio <donor> --track <id> --start <time>
|
|
315
473
|
[--duration 60] [--delay-ms 0] [--output <file>] [--force]
|
|
316
|
-
tracksmith mux --video <target> --
|
|
317
|
-
[--track <id>] [--delay-ms 0] [--language eng] [--name <title>]
|
|
318
|
-
[--
|
|
474
|
+
tracksmith mux --video <target> --output <file> [--dry-run] [--force]
|
|
475
|
+
--audio <mka-or-mkv> [--track <id>] [--delay-ms 0] [--language eng] [--name <title>] [--default]
|
|
476
|
+
[--audio <next> ...]
|
|
319
477
|
|
|
320
478
|
Notes:
|
|
479
|
+
Repeat --audio to add several tracks; per-track flags apply to the most recent --audio.
|
|
480
|
+
--default may be set on at most one --audio; it also clears the target's existing default audio flags.
|
|
481
|
+
--dry-run prints the planned track layout without writing.
|
|
321
482
|
--track is always the MKVToolNix track ID shown by "tracksmith inspect".
|
|
483
|
+
edit changes metadata in place without remuxing. --name "" and --title "" clear the value
|
|
484
|
+
(in Windows PowerShell use --name= and --title=; empty "" arguments get dropped).
|
|
485
|
+
--title sets the file title and cannot be combined with --track edits.
|
|
322
486
|
Positive --delay-ms delays audio; negative advances it (e.g. --delay-ms -250).
|
|
323
487
|
Times accept seconds (90, 90.5) or HH:MM:SS[.ms].
|
|
324
488
|
Check sync near the beginning, middle, and end; a changing offset cannot be fixed by one delay.
|
|
325
|
-
Requires ffmpeg and MKVToolNix (mkvmerge) on PATH.`;
|
|
489
|
+
Requires ffmpeg and MKVToolNix (mkvmerge, mkvpropedit) on PATH.`;
|
|
326
490
|
function parseIntStrict(value, flag) {
|
|
327
491
|
if (value === void 0) throw new CliError(`${flag} is required.`);
|
|
328
492
|
if (!/^-?\d+$/.test(value)) throw new CliError(`${flag} must be an integer, got "${value}".`);
|
|
@@ -440,46 +604,141 @@ async function runCli(argv, deps, stdout = console.log) {
|
|
|
440
604
|
return 0;
|
|
441
605
|
}
|
|
442
606
|
case "mux": {
|
|
443
|
-
const { values } = parseOrCliError(() => parseArgs({
|
|
607
|
+
const { values, tokens } = parseOrCliError(() => parseArgs({
|
|
444
608
|
args: rest,
|
|
445
609
|
options: {
|
|
446
610
|
video: { type: "string" },
|
|
447
|
-
|
|
448
|
-
|
|
611
|
+
output: { type: "string" },
|
|
612
|
+
force: {
|
|
613
|
+
type: "boolean",
|
|
614
|
+
default: false
|
|
615
|
+
},
|
|
616
|
+
"dry-run": {
|
|
617
|
+
type: "boolean",
|
|
618
|
+
default: false
|
|
619
|
+
},
|
|
620
|
+
audio: {
|
|
621
|
+
type: "string",
|
|
622
|
+
multiple: true
|
|
623
|
+
},
|
|
624
|
+
track: {
|
|
625
|
+
type: "string",
|
|
626
|
+
multiple: true
|
|
627
|
+
},
|
|
449
628
|
"delay-ms": {
|
|
450
629
|
type: "string",
|
|
451
|
-
|
|
630
|
+
multiple: true
|
|
452
631
|
},
|
|
453
632
|
language: {
|
|
454
633
|
type: "string",
|
|
455
|
-
|
|
634
|
+
multiple: true
|
|
456
635
|
},
|
|
457
|
-
name: {
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
default: false
|
|
636
|
+
name: {
|
|
637
|
+
type: "string",
|
|
638
|
+
multiple: true
|
|
461
639
|
},
|
|
462
|
-
|
|
463
|
-
force: {
|
|
640
|
+
default: {
|
|
464
641
|
type: "boolean",
|
|
465
|
-
|
|
642
|
+
multiple: true
|
|
466
643
|
}
|
|
467
644
|
},
|
|
468
|
-
strict: true
|
|
645
|
+
strict: true,
|
|
646
|
+
tokens: true
|
|
469
647
|
}));
|
|
470
|
-
|
|
648
|
+
const groups = [];
|
|
649
|
+
for (const token of tokens) {
|
|
650
|
+
if (token.kind !== "option") continue;
|
|
651
|
+
if (token.name === "audio") {
|
|
652
|
+
groups.push({ audio: token.value });
|
|
653
|
+
continue;
|
|
654
|
+
}
|
|
655
|
+
if (token.name !== "track" && token.name !== "delay-ms" && token.name !== "language" && token.name !== "name" && token.name !== "default") continue;
|
|
656
|
+
const group = groups.at(-1);
|
|
657
|
+
if (group === void 0) throw new CliError(`--${token.name} must come after an --audio input.`);
|
|
658
|
+
switch (token.name) {
|
|
659
|
+
case "track":
|
|
660
|
+
if (group.track !== void 0) throw new CliError(`Duplicate --track for ${describeAudioGroup(groups.length - 1, group.audio)}.`);
|
|
661
|
+
group.track = token.value;
|
|
662
|
+
break;
|
|
663
|
+
case "delay-ms":
|
|
664
|
+
if (group.delayMs !== void 0) throw new CliError(`Duplicate --delay-ms for ${describeAudioGroup(groups.length - 1, group.audio)}.`);
|
|
665
|
+
group.delayMs = token.value;
|
|
666
|
+
break;
|
|
667
|
+
case "language":
|
|
668
|
+
if (group.language !== void 0) throw new CliError(`Duplicate --language for ${describeAudioGroup(groups.length - 1, group.audio)}.`);
|
|
669
|
+
group.language = token.value;
|
|
670
|
+
break;
|
|
671
|
+
case "name":
|
|
672
|
+
if (group.name !== void 0) throw new CliError(`Duplicate --name for ${describeAudioGroup(groups.length - 1, group.audio)}.`);
|
|
673
|
+
group.name = token.value;
|
|
674
|
+
break;
|
|
675
|
+
case "default":
|
|
676
|
+
if (group.makeDefault !== void 0) throw new CliError(`Duplicate --default for ${describeAudioGroup(groups.length - 1, group.audio)}.`);
|
|
677
|
+
group.makeDefault = true;
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
if (!values.video || groups.length === 0) throw new CliError("mux requires --video and --audio.");
|
|
471
681
|
if (!values.output) throw new CliError("mux requires --output (no default output name for the final file).");
|
|
472
|
-
|
|
682
|
+
const result = await muxCommand({
|
|
473
683
|
video: values.video,
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
684
|
+
tracks: groups.map((group) => ({
|
|
685
|
+
audio: group.audio,
|
|
686
|
+
track: group.track === void 0 ? void 0 : parseTrack(group.track),
|
|
687
|
+
delayMs: parseIntStrict(group.delayMs ?? "0", "--delay-ms"),
|
|
688
|
+
language: group.language ?? "eng",
|
|
689
|
+
name: group.name,
|
|
690
|
+
makeDefault: group.makeDefault ?? false
|
|
691
|
+
})),
|
|
480
692
|
output: values.output,
|
|
481
|
-
force: values.force ?? false
|
|
482
|
-
|
|
693
|
+
force: values.force ?? false,
|
|
694
|
+
dryRun: values["dry-run"] ?? false
|
|
695
|
+
}, deps);
|
|
696
|
+
if (result.kind === "dry-run") stdout(result.plan);
|
|
697
|
+
else stdout(`Wrote ${result.output}`);
|
|
698
|
+
return 0;
|
|
699
|
+
}
|
|
700
|
+
case "edit": {
|
|
701
|
+
const { values, positionals } = parseOrCliError(() => parseArgs({
|
|
702
|
+
args: rest,
|
|
703
|
+
options: {
|
|
704
|
+
track: { type: "string" },
|
|
705
|
+
name: { type: "string" },
|
|
706
|
+
language: { type: "string" },
|
|
707
|
+
default: { type: "boolean" },
|
|
708
|
+
"no-default": { type: "boolean" },
|
|
709
|
+
forced: { type: "boolean" },
|
|
710
|
+
"no-forced": { type: "boolean" },
|
|
711
|
+
title: { type: "string" }
|
|
712
|
+
},
|
|
713
|
+
allowPositionals: true,
|
|
714
|
+
strict: true
|
|
715
|
+
}));
|
|
716
|
+
const file = requireFilePositional(positionals, "edit");
|
|
717
|
+
const hasTrackFlags = values.track !== void 0 || values.name !== void 0 || values.language !== void 0 || values.default !== void 0 || values["no-default"] !== void 0 || values.forced !== void 0 || values["no-forced"] !== void 0;
|
|
718
|
+
if (values.title !== void 0 && hasTrackFlags) throw new CliError("--title cannot be combined with --track edits. Run them as separate invocations.");
|
|
719
|
+
if (values.default !== void 0 && values["no-default"] !== void 0) throw new CliError("Pass either --default or --no-default, not both.");
|
|
720
|
+
if (values.forced !== void 0 && values["no-forced"] !== void 0) throw new CliError("Pass either --forced or --no-forced, not both.");
|
|
721
|
+
if (values.language === "") throw new CliError("--language cannot be empty. Use --language und for undetermined.");
|
|
722
|
+
let warning;
|
|
723
|
+
if (values.title !== void 0) warning = await editCommand({
|
|
724
|
+
kind: "title",
|
|
725
|
+
file,
|
|
726
|
+
title: values.title
|
|
727
|
+
}, deps);
|
|
728
|
+
else {
|
|
729
|
+
if (values.track === void 0) throw new CliError("edit requires --track (or --title for the file title).");
|
|
730
|
+
warning = await editCommand({
|
|
731
|
+
kind: "track",
|
|
732
|
+
file,
|
|
733
|
+
track: parseTrack(values.track),
|
|
734
|
+
name: values.name,
|
|
735
|
+
language: values.language,
|
|
736
|
+
isDefault: values.default ?? (values["no-default"] !== void 0 ? false : void 0),
|
|
737
|
+
isForced: values.forced ?? (values["no-forced"] !== void 0 ? false : void 0)
|
|
738
|
+
}, deps);
|
|
739
|
+
}
|
|
740
|
+
if (warning !== void 0) console.error(`tracksmith: ${warning}`);
|
|
741
|
+
stdout(`Edited ${file}`);
|
|
483
742
|
return 0;
|
|
484
743
|
}
|
|
485
744
|
default: throw new CliError(`Unknown command "${command}".\n\n${USAGE}`);
|
|
@@ -517,13 +776,19 @@ function createConfirmPrompt(options = {}) {
|
|
|
517
776
|
}
|
|
518
777
|
//#endregion
|
|
519
778
|
//#region src/tools.ts
|
|
520
|
-
const TOOLS = [
|
|
779
|
+
const TOOLS = [
|
|
780
|
+
"ffmpeg",
|
|
781
|
+
"mkvmerge",
|
|
782
|
+
"mkvpropedit"
|
|
783
|
+
];
|
|
521
784
|
function isToolName(value) {
|
|
522
785
|
return TOOLS.some((tool) => tool === value);
|
|
523
786
|
}
|
|
787
|
+
const MKVTOOLNIX_HINT = "Windows: winget install MoritzBunkus.MKVToolNix or scoop install mkvtoolnix | macOS: brew install mkvtoolnix | Linux: sudo apt install mkvtoolnix or sudo pacman -S mkvtoolnix-cli";
|
|
524
788
|
const INSTALL_HINTS = {
|
|
525
789
|
ffmpeg: "Windows: winget install Gyan.FFmpeg or scoop install ffmpeg | macOS: brew install ffmpeg | Linux: sudo apt install ffmpeg or sudo pacman -S ffmpeg",
|
|
526
|
-
mkvmerge:
|
|
790
|
+
mkvmerge: MKVTOOLNIX_HINT,
|
|
791
|
+
mkvpropedit: MKVTOOLNIX_HINT
|
|
527
792
|
};
|
|
528
793
|
function installHint(tool) {
|
|
529
794
|
return INSTALL_HINTS[tool];
|
|
@@ -566,7 +831,13 @@ var ProcessRunner = class {
|
|
|
566
831
|
process.once("error", (error) => {
|
|
567
832
|
reject(processStartError(executable, error));
|
|
568
833
|
});
|
|
569
|
-
process.once("close", (exitCode) => {
|
|
834
|
+
process.once("close", (exitCode, signal) => {
|
|
835
|
+
if (signal !== null) {
|
|
836
|
+
const output = [stdout, stderr].map((stream) => stream.trim()).filter(Boolean).join("\n");
|
|
837
|
+
const diagnostics = output === "" ? "" : `\nOutput before termination:\n${output}`;
|
|
838
|
+
reject(new CliError(`${executable} was terminated by signal ${signal}.${diagnostics}`));
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
570
841
|
resolve({
|
|
571
842
|
exitCode: exitCode ?? 1,
|
|
572
843
|
stdout,
|
|
@@ -582,7 +853,7 @@ process.exitCode = await runCli(process.argv.slice(2), {
|
|
|
582
853
|
runner: new ProcessRunner(),
|
|
583
854
|
isTTY: process.stdin.isTTY ?? false,
|
|
584
855
|
stderrIsTTY: process.stderr.isTTY ?? false,
|
|
585
|
-
confirm: createConfirmPrompt()
|
|
856
|
+
confirm: createConfirmPrompt({ output: process.stderr })
|
|
586
857
|
});
|
|
587
858
|
//#endregion
|
|
588
859
|
export {};
|
package/dist/tracksmith.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tracksmith.js","names":[],"sources":["../src/types.ts","../src/output.ts","../src/paths.ts","../src/probe.ts","../src/commands/extract.ts","../src/commands/inspect.ts","../src/commands/mux.ts","../src/commands/test-clip.ts","../src/cli.ts","../src/prompt.ts","../src/tools.ts","../src/runner.ts","../src/tracksmith.ts"],"sourcesContent":["export interface RunResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface RunOptions {\n stream?: 'stdout' | 'stderr';\n}\n\nexport interface Runner {\n run(argv: readonly string[], options?: RunOptions): Promise<RunResult>;\n}\n\nexport interface Track {\n id: number;\n type: string;\n codec: string;\n language?: string;\n name?: string;\n channels?: number;\n isDefault: boolean;\n}\n\nexport class CliError extends Error {\n constructor(\n message: string,\n readonly exitCode: number = 1\n ) {\n super(message);\n this.name = 'CliError';\n }\n}\n\nexport interface CommandDeps {\n runner: Runner;\n /** stdin is interactive — gates confirmation prompts. */\n isTTY: boolean;\n /** stderr is interactive — gates live tool progress output. */\n stderrIsTTY: boolean;\n confirm: (message: string) => Promise<boolean>;\n exists?: (path: string) => boolean;\n}\n","import { existsSync, statSync } from 'node:fs';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { CliError } from './types.ts';\n\nexport function stemPath(file: string): string {\n return join(dirname(file), basename(file, extname(file)));\n}\n\nexport function defaultExtractOutput(source: string, trackId: number): string {\n return `${stemPath(source)}.track${trackId}.mka`;\n}\n\nexport function defaultTestClipOutput(video: string): string {\n return `${stemPath(video)}.sync-test.mkv`;\n}\n\nexport function requireInputFile(\n path: string,\n exists: (path: string) => boolean = existsSync\n): void {\n if (!exists(path)) throw new CliError(`Input file not found: \"${path}\".`);\n}\n\nexport interface OverwriteContext {\n force: boolean;\n isTTY: boolean;\n confirm: (message: string) => Promise<boolean>;\n exists?: (path: string) => boolean;\n inputs?: readonly string[];\n}\n\nfunction sameFile(left: string, right: string): boolean {\n try {\n const leftStat = statSync(left, { bigint: true });\n const rightStat = statSync(right, { bigint: true });\n return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;\n } catch {\n return false;\n }\n}\n\nexport async function ensureWritable(path: string, ctx: OverwriteContext): Promise<void> {\n const aliasedInput = ctx.inputs?.find((input) => sameFile(path, input));\n if (aliasedInput !== undefined) {\n throw new CliError(\n `Output \"${path}\" is the same file as input \"${aliasedInput}\". Choose a different output path.`\n );\n }\n\n const exists = ctx.exists ?? existsSync;\n if (!exists(path)) return;\n if (ctx.force) return;\n if (!ctx.isTTY) {\n throw new CliError(\n `Output \"${path}\" already exists. Pass --force to overwrite (cannot prompt in a non-interactive session).`\n );\n }\n const approved = await ctx.confirm(`Output \"${path}\" already exists. Overwrite?`);\n if (!approved) throw new CliError('Aborted: existing output left untouched.');\n}\n","import { isAbsolute, resolve } from 'node:path';\n\nconst TOOL_OPERATOR_PREFIXES = '-+@=()[]';\n\nexport function toolPath(path: string): string {\n if (isAbsolute(path) || !TOOL_OPERATOR_PREFIXES.includes(path[0] ?? '')) return path;\n return resolve(path);\n}\n","import type { Runner, Track } from './types.ts';\n\nimport * as z from 'zod/mini';\n\nimport { toolPath } from './paths.ts';\nimport { CliError } from './types.ts';\n\nconst mkvmergePropertiesSchema = z.object({\n language: z.optional(z.string()),\n track_name: z.optional(z.string()),\n audio_channels: z.optional(z.number()),\n default_track: z.optional(z.boolean())\n});\n\nconst mkvmergeTrackSchema = z.object({\n id: z.int(),\n type: z.string(),\n codec: z.string(),\n properties: z.optional(mkvmergePropertiesSchema)\n});\n\nconst mkvmergeOutputSchema = z.object({\n tracks: z.array(mkvmergeTrackSchema)\n});\n\nexport function parseMkvmergeJson(json: string): Track[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new CliError('Could not parse mkvmerge -J output as JSON.');\n }\n const result = z.safeParse(mkvmergeOutputSchema, parsed);\n if (!result.success) throw new CliError('mkvmerge -J output has an unexpected structure.');\n\n return result.data.tracks.map((track) => ({\n id: track.id,\n type: track.type,\n codec: track.codec,\n language: track.properties?.language,\n name: track.properties?.track_name,\n channels: track.properties?.audio_channels,\n isDefault: track.properties?.default_track ?? false\n }));\n}\n\nexport async function probeTracks(runner: Runner, file: string): Promise<Track[]> {\n const result = await runner.run(['mkvmerge', '-J', toolPath(file)]);\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge could not read \"${file}\" (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return parseMkvmergeJson(result.stdout);\n}\n\nfunction validAudioIds(tracks: Track[]): string {\n const ids = tracks.filter((track) => track.type === 'audio').map((track) => track.id);\n return `Valid audio track IDs: ${ids.length > 0 ? ids.join(', ') : 'none'}`;\n}\n\nexport function requireAudioTrack(tracks: Track[], id: number): Track {\n const track = tracks.find((candidate) => candidate.id === id);\n if (!track) throw new CliError(`Track ${id} does not exist. ${validAudioIds(tracks)}`);\n if (track.type !== 'audio')\n throw new CliError(`Track ${id} is ${track.type}, not audio. ${validAudioIds(tracks)}`);\n return track;\n}\n\nexport function audioRelativeIndex(tracks: Track[], id: number): number {\n const index = tracks\n .filter((track) => track.type === 'audio')\n .findIndex((track) => track.id === id);\n if (index === -1)\n throw new CliError(`Track ${id} is not an audio track. ${validAudioIds(tracks)}`);\n return index;\n}\n","import type { CommandDeps } from '../types.ts';\n\nimport { defaultExtractOutput, ensureWritable, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport function buildExtractArgs(opts: { file: string; track: number; output: string }): string[] {\n return [\n 'mkvmerge',\n '-o',\n toolPath(opts.output),\n '--audio-tracks',\n String(opts.track),\n '--no-video',\n '--no-subtitles',\n '--no-buttons',\n '--no-chapters',\n '--no-attachments',\n '--no-global-tags',\n toolPath(opts.file)\n ];\n}\n\nexport async function extractCommand(\n opts: { file: string; track: number; output?: string; force: boolean },\n deps: CommandDeps\n): Promise<string> {\n requireInputFile(opts.file, deps.exists);\n const tracks = await probeTracks(deps.runner, opts.file);\n requireAudioTrack(tracks, opts.track);\n const output = opts.output ?? defaultExtractOutput(opts.file, opts.track);\n await ensureWritable(output, {\n force: opts.force,\n isTTY: deps.isTTY,\n confirm: deps.confirm,\n exists: deps.exists,\n inputs: [opts.file]\n });\n const result = await deps.runner.run(\n buildExtractArgs({ file: opts.file, track: opts.track, output }),\n deps.stderrIsTTY ? { stream: 'stdout' } : undefined\n );\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return output;\n}\n","import type { CommandDeps, Track } from '../types.ts';\n\nimport { requireInputFile } from '../output.ts';\nimport { probeTracks } from '../probe.ts';\n\nexport function formatTrackTable(tracks: Track[]): string {\n const header = ['ID', 'TYPE', 'CODEC', 'LANG', 'CH', 'DEFAULT', 'NAME'];\n const rows = tracks.map((track) => [\n String(track.id),\n track.type,\n track.codec,\n track.language ?? '-',\n track.channels !== undefined ? String(track.channels) : '-',\n track.isDefault ? 'yes' : '-',\n track.name ?? '-'\n ]);\n const all = [header, ...rows];\n const widths = header.map((_, column) => Math.max(...all.map((row) => row[column]!.length)));\n return all\n .map((row) =>\n row\n .map((cell, column) => cell.padEnd(widths[column]!))\n .join(' ')\n .trimEnd()\n )\n .join('\\n');\n}\n\nexport async function inspectCommand(file: string, deps: CommandDeps): Promise<string> {\n requireInputFile(file, deps.exists);\n return formatTrackTable(await probeTracks(deps.runner, file));\n}\n","import type { CommandDeps, Track } from '../types.ts';\n\nimport { ensureWritable, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport function resolveAudioTrackId(tracks: Track[], requested: number | undefined): number {\n if (requested !== undefined) return requireAudioTrack(tracks, requested).id;\n const audio = tracks.filter((track) => track.type === 'audio');\n if (audio.length === 1) return audio[0]!.id;\n if (audio.length === 0) throw new CliError('The audio input contains no audio tracks.');\n throw new CliError(\n `The audio input contains ${audio.length} audio tracks; pass --track <id> (see: tracksmith inspect <file>).`\n );\n}\n\nexport function buildMuxArgs(a: {\n video: string;\n audio: string;\n audioTrackId: number;\n delayMs: number;\n language?: string;\n trackName?: string;\n makeDefault: boolean;\n output: string;\n}): string[] {\n const id = String(a.audioTrackId);\n const args = [\n 'mkvmerge',\n '-o',\n toolPath(a.output),\n toolPath(a.video),\n '--audio-tracks',\n id,\n '--no-video',\n '--no-subtitles',\n '--no-buttons',\n '--no-chapters',\n '--no-attachments',\n '--no-global-tags'\n ];\n if (a.delayMs !== 0) args.push('--sync', `${id}:${a.delayMs}`);\n if (a.language !== undefined) args.push('--language', `${id}:${a.language}`);\n if (a.trackName !== undefined) args.push('--track-name', `${id}:${a.trackName}`);\n args.push('--default-track-flag', `${id}:${a.makeDefault ? 'yes' : 'no'}`);\n args.push(toolPath(a.audio));\n return args;\n}\n\nexport async function muxCommand(\n opts: {\n video: string;\n audio: string;\n track?: number;\n delayMs: number;\n language?: string;\n name?: string;\n makeDefault: boolean;\n output: string;\n force: boolean;\n },\n deps: CommandDeps\n): Promise<string> {\n requireInputFile(opts.video, deps.exists);\n requireInputFile(opts.audio, deps.exists);\n const audioTracks = await probeTracks(deps.runner, opts.audio);\n const audioTrackId = resolveAudioTrackId(audioTracks, opts.track);\n await ensureWritable(opts.output, {\n force: opts.force,\n isTTY: deps.isTTY,\n confirm: deps.confirm,\n exists: deps.exists,\n inputs: [opts.video, opts.audio]\n });\n const result = await deps.runner.run(\n buildMuxArgs({\n video: opts.video,\n audio: opts.audio,\n audioTrackId,\n delayMs: opts.delayMs,\n language: opts.language,\n trackName: opts.name,\n makeDefault: opts.makeDefault,\n output: opts.output\n }),\n deps.stderrIsTTY ? { stream: 'stdout' } : undefined\n );\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return opts.output;\n}\n","import type { CommandDeps } from '../types.ts';\n\nimport { defaultTestClipOutput, ensureWritable, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { audioRelativeIndex, probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport function parseTimeToSeconds(value: string): number {\n if (/^\\d+(\\.\\d+)?$/.test(value)) return Number(value);\n const match = /^(\\d+):([0-5]?\\d):([0-5]?\\d(?:\\.\\d+)?)$/.exec(value);\n if (!match)\n throw new CliError(`Invalid time \"${value}\". Use seconds (90 or 90.5) or HH:MM:SS[.ms].`);\n return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]);\n}\n\nexport function buildTestClipArgs(a: {\n video: string;\n audio: string;\n audioStreamIndex: number;\n startSeconds: number;\n durationSeconds: number;\n delayMs: number;\n output: string;\n}): string[] {\n // -y is safe: our own overwrite policy (ensureWritable) has already run.\n return [\n 'ffmpeg',\n '-hide_banner',\n '-nostdin',\n '-ss',\n String(a.startSeconds),\n '-i',\n toolPath(a.video),\n '-itsoffset',\n String(a.delayMs / 1000),\n '-ss',\n String(a.startSeconds),\n '-i',\n toolPath(a.audio),\n '-map',\n '0:v:0',\n '-map',\n `1:a:${a.audioStreamIndex}`,\n '-t',\n String(a.durationSeconds),\n '-c:v',\n 'libx264',\n '-preset',\n 'veryfast',\n '-crf',\n '18',\n '-c:a',\n 'copy',\n '-sn',\n '-abort_on',\n 'empty_output_stream',\n '-y',\n toolPath(a.output)\n ];\n}\n\nexport async function testClipCommand(\n opts: {\n video: string;\n audio: string;\n track: number;\n start: string;\n duration: string;\n delayMs: number;\n output?: string;\n force: boolean;\n },\n deps: CommandDeps\n): Promise<string> {\n requireInputFile(opts.video, deps.exists);\n requireInputFile(opts.audio, deps.exists);\n const startSeconds = parseTimeToSeconds(opts.start);\n const durationSeconds = parseTimeToSeconds(opts.duration);\n if (durationSeconds <= 0) throw new CliError('--duration must be greater than zero.');\n const donorTracks = await probeTracks(deps.runner, opts.audio);\n requireAudioTrack(donorTracks, opts.track);\n const output = opts.output ?? defaultTestClipOutput(opts.video);\n await ensureWritable(output, {\n force: opts.force,\n isTTY: deps.isTTY,\n confirm: deps.confirm,\n exists: deps.exists,\n inputs: [opts.video, opts.audio]\n });\n const result = await deps.runner.run(\n buildTestClipArgs({\n video: opts.video,\n audio: opts.audio,\n audioStreamIndex: audioRelativeIndex(donorTracks, opts.track),\n startSeconds,\n durationSeconds,\n delayMs: opts.delayMs,\n output\n }),\n deps.stderrIsTTY ? { stream: 'stderr' } : undefined\n );\n if (result.exitCode !== 0) {\n throw new CliError(\n `ffmpeg failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return output;\n}\n","import { parseArgs } from 'node:util';\n\nimport type { CommandDeps } from './types.ts';\n\nimport { extractCommand } from './commands/extract.ts';\nimport { inspectCommand } from './commands/inspect.ts';\nimport { muxCommand } from './commands/mux.ts';\nimport { testClipCommand } from './commands/test-clip.ts';\nimport { CliError } from './types.ts';\n\nconst USAGE = `tracksmith — inspect, extract, sync-test, and mux Matroska audio tracks\n\nUsage:\n tracksmith inspect <file>\n tracksmith extract <file> --track <id> [--output <file>] [--force]\n tracksmith test-clip --video <target> --audio <donor> --track <id> --start <time>\n [--duration 60] [--delay-ms 0] [--output <file>] [--force]\n tracksmith mux --video <target> --audio <mka-or-mkv> --output <file>\n [--track <id>] [--delay-ms 0] [--language eng] [--name <title>]\n [--default] [--force]\n\nNotes:\n --track is always the MKVToolNix track ID shown by \"tracksmith inspect\".\n Positive --delay-ms delays audio; negative advances it (e.g. --delay-ms -250).\n Times accept seconds (90, 90.5) or HH:MM:SS[.ms].\n Check sync near the beginning, middle, and end; a changing offset cannot be fixed by one delay.\n Requires ffmpeg and MKVToolNix (mkvmerge) on PATH.`;\n\nfunction parseIntStrict(value: string | undefined, flag: string): number {\n if (value === undefined) throw new CliError(`${flag} is required.`);\n if (!/^-?\\d+$/.test(value)) throw new CliError(`${flag} must be an integer, got \"${value}\".`);\n return Number(value);\n}\n\nfunction parseTrack(value: string | undefined): number {\n const track = parseIntStrict(value, '--track');\n if (track < 0) throw new CliError(`--track must be >= 0, got ${track}.`);\n return track;\n}\n\nfunction requireFilePositional(positionals: string[], command: 'inspect' | 'extract'): string {\n if (positionals.length === 0) throw new CliError(`${command} requires a file argument.`);\n if (positionals.length > 1) throw new CliError(`${command} accepts exactly one file argument.`);\n return positionals[0]!;\n}\n\n// Wraps a parseArgs call so parse failures become CliErrors. Takes a thunk\n// (rather than the options object) so parseArgs keeps its precise generic\n// inference over each command's inline options literal.\nfunction parseOrCliError<T>(parseFn: () => T): T {\n try {\n return parseFn();\n } catch (error) {\n throw new CliError(error instanceof Error ? error.message : String(error));\n }\n}\n\n// parseArgs treats a leading-dash value token (\"-250\") as an option, so fold\n// `--delay-ms -250` into the equals form it does accept.\nfunction normalizeDelayMs(args: string[]): string[] {\n const out: string[] = [];\n for (let i = 0; i < args.length; i += 1) {\n const arg = args[i]!;\n const next = args[i + 1];\n if (arg === '--delay-ms' && next !== undefined && /^-\\d+$/.test(next)) {\n out.push(`--delay-ms=${next}`);\n i += 1;\n } else {\n out.push(arg);\n }\n }\n return out;\n}\n\nexport async function runCli(\n argv: string[],\n deps: CommandDeps,\n stdout: (line: string) => void = console.log\n): Promise<number> {\n const [command, ...rawRest] = argv;\n const rest = normalizeDelayMs(rawRest);\n try {\n switch (command) {\n case undefined: {\n stdout(USAGE);\n return 1;\n }\n case '--help':\n case '-h': {\n stdout(USAGE);\n return 0;\n }\n case 'inspect': {\n const { positionals } = parseOrCliError(() =>\n parseArgs({ args: rest, options: {}, allowPositionals: true, strict: true })\n );\n const file = requireFilePositional(positionals, 'inspect');\n stdout(await inspectCommand(file, deps));\n return 0;\n }\n case 'extract': {\n const { values, positionals } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n track: { type: 'string' },\n output: { type: 'string' },\n force: { type: 'boolean', default: false }\n },\n allowPositionals: true,\n strict: true\n })\n );\n const file = requireFilePositional(positionals, 'extract');\n const output = await extractCommand(\n {\n file,\n track: parseTrack(values.track),\n output: values.output,\n force: values.force ?? false\n },\n deps\n );\n stdout(`Wrote ${output}`);\n return 0;\n }\n case 'test-clip': {\n const { values } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n video: { type: 'string' },\n audio: { type: 'string' },\n track: { type: 'string' },\n start: { type: 'string' },\n duration: { type: 'string', default: '60' },\n 'delay-ms': { type: 'string', default: '0' },\n output: { type: 'string' },\n force: { type: 'boolean', default: false }\n },\n strict: true\n })\n );\n if (!values.video || !values.audio)\n throw new CliError('test-clip requires --video and --audio.');\n if (!values.start) throw new CliError('test-clip requires --start.');\n const output = await testClipCommand(\n {\n video: values.video,\n audio: values.audio,\n track: parseTrack(values.track),\n start: values.start,\n duration: values.duration ?? '60',\n delayMs: parseIntStrict(values['delay-ms'] ?? '0', '--delay-ms'),\n output: values.output,\n force: values.force ?? false\n },\n deps\n );\n stdout(`Wrote ${output}`);\n return 0;\n }\n case 'mux': {\n const { values } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n video: { type: 'string' },\n audio: { type: 'string' },\n track: { type: 'string' },\n 'delay-ms': { type: 'string', default: '0' },\n language: { type: 'string', default: 'eng' },\n name: { type: 'string' },\n default: { type: 'boolean', default: false },\n output: { type: 'string' },\n force: { type: 'boolean', default: false }\n },\n strict: true\n })\n );\n if (!values.video || !values.audio) throw new CliError('mux requires --video and --audio.');\n if (!values.output)\n throw new CliError('mux requires --output (no default output name for the final file).');\n const output = await muxCommand(\n {\n video: values.video,\n audio: values.audio,\n track: values.track === undefined ? undefined : parseTrack(values.track),\n delayMs: parseIntStrict(values['delay-ms'] ?? '0', '--delay-ms'),\n language: values.language,\n name: values.name,\n makeDefault: values.default ?? false,\n output: values.output,\n force: values.force ?? false\n },\n deps\n );\n stdout(`Wrote ${output}`);\n return 0;\n }\n default:\n throw new CliError(`Unknown command \"${command}\".\\n\\n${USAGE}`);\n }\n } catch (error) {\n if (error instanceof CliError) {\n console.error(`tracksmith: ${error.message}`);\n return error.exitCode;\n }\n throw error;\n }\n}\n","import { once } from 'node:events';\nimport { createInterface } from 'node:readline/promises';\n\nimport type { Readable, Writable } from 'node:stream';\n\nexport interface ConfirmPromptOptions {\n input?: Readable;\n output?: Writable;\n}\n\nexport function createConfirmPrompt(\n options: ConfirmPromptOptions = {}\n): (message: string) => Promise<boolean> {\n const input = options.input ?? process.stdin;\n const output = options.output ?? process.stdout;\n\n return async (message: string): Promise<boolean> => {\n const readline = createInterface({ input, output });\n\n try {\n const closed = once(readline, 'close').then(() => ({ kind: 'eof' }) as const);\n const answered = readline.question(`${message} [y/N] `).then(\n (value) => ({ kind: 'answer', value }) as const,\n () => ({ kind: 'eof' }) as const\n );\n const result = await Promise.race([answered, closed]);\n\n return result.kind === 'answer' && /^y(?:es)?$/i.test(result.value.trim());\n } finally {\n readline.close();\n }\n };\n}\n","export const TOOLS = ['ffmpeg', 'mkvmerge'] as const;\n\nexport type ToolName = (typeof TOOLS)[number];\n\nexport function isToolName(value: string): value is ToolName {\n return TOOLS.some((tool) => tool === value);\n}\n\nconst INSTALL_HINTS = {\n ffmpeg:\n 'Windows: winget install Gyan.FFmpeg or scoop install ffmpeg | macOS: brew install ffmpeg | Linux: sudo apt install ffmpeg or sudo pacman -S ffmpeg',\n mkvmerge:\n 'Windows: winget install MoritzBunkus.MKVToolNix or scoop install mkvtoolnix | macOS: brew install mkvtoolnix | Linux: sudo apt install mkvtoolnix or sudo pacman -S mkvtoolnix-cli'\n} satisfies Record<ToolName, string>;\n\nexport function installHint(tool: ToolName): string {\n return INSTALL_HINTS[tool];\n}\n","import { spawn } from 'node:child_process';\n\nimport type { RunOptions, Runner, RunResult } from './types.ts';\n\nimport { installHint, isToolName } from './tools.ts';\nimport { CliError } from './types.ts';\n\nexport function processStartError(program: string, error: unknown): CliError {\n if (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n error.code === 'ENOENT' &&\n isToolName(program)\n ) {\n return new CliError(\n `${program} is required but was not found on PATH. ${installHint(program)}`\n );\n }\n\n const message = error instanceof Error ? error.message : String(error);\n return new CliError(`Failed to start ${program}: ${message}`);\n}\n\nconst writeToCliStderr = (chunk: string): void => {\n globalThis.process.stderr.write(chunk);\n};\n\nexport class ProcessRunner implements Runner {\n private readonly sink: (chunk: string) => void;\n\n constructor(options: { sink?: (chunk: string) => void } = {}) {\n this.sink = options.sink ?? writeToCliStderr;\n }\n\n async run(argv: readonly string[], options?: RunOptions): Promise<RunResult> {\n const [executable, ...args] = argv;\n if (executable === undefined) {\n throw new CliError('Could not start process: missing executable');\n }\n\n return await new Promise((resolve, reject) => {\n const process = spawn(executable, args, {\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let stdout = '';\n let stderr = '';\n\n process.stdout.setEncoding('utf8');\n process.stderr.setEncoding('utf8');\n process.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n if (options?.stream === 'stdout') this.sink(chunk);\n });\n process.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n if (options?.stream === 'stderr') this.sink(chunk);\n });\n process.once('error', (error) => {\n reject(processStartError(executable, error));\n });\n process.once('close', (exitCode) => {\n resolve({ exitCode: exitCode ?? 1, stdout, stderr });\n });\n });\n }\n}\n","#!/usr/bin/env node\n\nimport { runCli } from './cli.ts';\nimport { createConfirmPrompt } from './prompt.ts';\nimport { ProcessRunner } from './runner.ts';\n\nprocess.exitCode = await runCli(process.argv.slice(2), {\n runner: new ProcessRunner(),\n isTTY: process.stdin.isTTY ?? false,\n stderrIsTTY: process.stderr.isTTY ?? false,\n confirm: createConfirmPrompt()\n});\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tracksmith.js","names":[],"sources":["../src/types.ts","../src/output.ts","../src/paths.ts","../src/probe.ts","../src/commands/edit.ts","../src/commands/extract.ts","../src/commands/inspect.ts","../src/commands/mux.ts","../src/commands/test-clip.ts","../src/cli.ts","../src/prompt.ts","../src/tools.ts","../src/runner.ts","../src/tracksmith.ts"],"sourcesContent":["export interface RunResult {\n exitCode: number;\n stdout: string;\n stderr: string;\n}\n\nexport interface RunOptions {\n stream?: 'stdout' | 'stderr';\n}\n\nexport interface Runner {\n run(argv: readonly string[], options?: RunOptions): Promise<RunResult>;\n}\n\nexport interface Track {\n id: number;\n type: string;\n codec: string;\n language?: string;\n name?: string;\n channels?: number;\n isDefault: boolean;\n isForced: boolean;\n}\n\nexport class CliError extends Error {\n constructor(\n message: string,\n readonly exitCode: number = 1\n ) {\n super(message);\n this.name = 'CliError';\n }\n}\n\nexport interface CommandDeps {\n runner: Runner;\n /** stdin is interactive — gates confirmation prompts. */\n isTTY: boolean;\n /** stderr is interactive — gates live tool progress output. */\n stderrIsTTY: boolean;\n confirm: (message: string) => Promise<boolean>;\n exists?: (path: string) => boolean;\n}\n","import { existsSync, statSync } from 'node:fs';\nimport { basename, dirname, extname, join } from 'node:path';\n\nimport { CliError } from './types.ts';\n\nexport function stemPath(file: string): string {\n return join(dirname(file), basename(file, extname(file)));\n}\n\nexport function defaultExtractOutput(source: string, trackId: number): string {\n return `${stemPath(source)}.track${trackId}.mka`;\n}\n\nexport function defaultTestClipOutput(video: string): string {\n return `${stemPath(video)}.sync-test.mkv`;\n}\n\nexport function requireInputFile(\n path: string,\n exists: (path: string) => boolean = existsSync\n): void {\n if (!exists(path)) throw new CliError(`Input file not found: \"${path}\".`);\n}\n\nexport interface OverwriteContext {\n force: boolean;\n isTTY: boolean;\n confirm: (message: string) => Promise<boolean>;\n exists?: (path: string) => boolean;\n inputs?: readonly string[];\n}\n\nfunction sameFile(left: string, right: string): boolean {\n try {\n const leftStat = statSync(left, { bigint: true });\n const rightStat = statSync(right, { bigint: true });\n return leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino;\n } catch {\n return false;\n }\n}\n\n/** Returns the input path the output aliases on disk (same dev/inode), if any. */\nexport function findAliasedInput(output: string, inputs: readonly string[]): string | undefined {\n return inputs.find((input) => sameFile(output, input));\n}\n\nexport async function ensureWritable(path: string, ctx: OverwriteContext): Promise<void> {\n const aliasedInput = findAliasedInput(path, ctx.inputs ?? []);\n if (aliasedInput !== undefined) {\n throw new CliError(\n `Output \"${path}\" is the same file as input \"${aliasedInput}\". Choose a different output path.`\n );\n }\n\n const exists = ctx.exists ?? existsSync;\n if (!exists(path)) return;\n if (ctx.force) return;\n if (!ctx.isTTY) {\n throw new CliError(\n `Output \"${path}\" already exists. Pass --force to overwrite (cannot prompt in a non-interactive session).`\n );\n }\n const approved = await ctx.confirm(`Output \"${path}\" already exists. Overwrite?`);\n if (!approved) throw new CliError('Aborted: existing output left untouched.');\n}\n","import { isAbsolute, resolve } from 'node:path';\n\nconst TOOL_OPERATOR_PREFIXES = '-+@=()[]';\n\nexport function toolPath(path: string): string {\n if (isAbsolute(path) || !TOOL_OPERATOR_PREFIXES.includes(path[0] ?? '')) return path;\n return resolve(path);\n}\n","import type { Runner, Track } from './types.ts';\n\nimport * as z from 'zod/mini';\n\nimport { toolPath } from './paths.ts';\nimport { CliError } from './types.ts';\n\nconst mkvmergePropertiesSchema = z.object({\n language: z.optional(z.string()),\n language_ietf: z.optional(z.string()),\n track_name: z.optional(z.string()),\n audio_channels: z.optional(z.number()),\n default_track: z.optional(z.boolean()),\n forced_track: z.optional(z.boolean())\n});\n\nconst mkvmergeTrackSchema = z.object({\n id: z.int(),\n type: z.string(),\n codec: z.string(),\n properties: z.optional(mkvmergePropertiesSchema)\n});\n\nconst mkvmergeContainerSchema = z.object({\n properties: z.optional(z.object({ title: z.optional(z.string()) }))\n});\n\nconst mkvmergeOutputSchema = z.object({\n // Container metadata is ancillary: an unexpected shape must not fail track\n // parsing, so it is validated separately and dropped on mismatch.\n container: z.optional(z.unknown()),\n tracks: z.array(mkvmergeTrackSchema)\n});\n\nfunction containerTitle(container: unknown): string | undefined {\n const result = z.safeParse(mkvmergeContainerSchema, container);\n return result.success ? result.data.properties?.title : undefined;\n}\n\nexport interface MkvFile {\n title?: string;\n tracks: Track[];\n}\n\nexport function parseMkvmergeFile(json: string): MkvFile {\n let parsed: unknown;\n try {\n parsed = JSON.parse(json);\n } catch {\n throw new CliError('Could not parse mkvmerge -J output as JSON.');\n }\n const result = z.safeParse(mkvmergeOutputSchema, parsed);\n if (!result.success) throw new CliError('mkvmerge -J output has an unexpected structure.');\n\n return {\n title: containerTitle(result.data.container),\n tracks: result.data.tracks.map((track) => ({\n id: track.id,\n type: track.type,\n codec: track.codec,\n language: track.properties?.language_ietf || track.properties?.language || undefined,\n name: track.properties?.track_name,\n channels: track.properties?.audio_channels,\n isDefault: track.properties?.default_track ?? false,\n isForced: track.properties?.forced_track ?? false\n }))\n };\n}\n\nexport function parseMkvmergeJson(json: string): Track[] {\n return parseMkvmergeFile(json).tracks;\n}\n\nexport async function probeFile(runner: Runner, file: string): Promise<MkvFile> {\n const result = await runner.run(['mkvmerge', '-J', toolPath(file)]);\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge could not read \"${file}\" (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return parseMkvmergeFile(result.stdout);\n}\n\nexport async function probeTracks(runner: Runner, file: string): Promise<Track[]> {\n return (await probeFile(runner, file)).tracks;\n}\n\nfunction validAudioIds(tracks: Track[]): string {\n const ids = tracks.filter((track) => track.type === 'audio').map((track) => track.id);\n return `Valid audio track IDs: ${ids.length > 0 ? ids.join(', ') : 'none'}`;\n}\n\nexport function requireAudioTrack(tracks: Track[], id: number): Track {\n const track = tracks.find((candidate) => candidate.id === id);\n if (!track) throw new CliError(`Track ${id} does not exist. ${validAudioIds(tracks)}`);\n if (track.type !== 'audio')\n throw new CliError(`Track ${id} is ${track.type}, not audio. ${validAudioIds(tracks)}`);\n return track;\n}\n\nexport function audioRelativeIndex(tracks: Track[], id: number): number {\n const index = tracks\n .filter((track) => track.type === 'audio')\n .findIndex((track) => track.id === id);\n if (index === -1)\n throw new CliError(`Track ${id} is not an audio track. ${validAudioIds(tracks)}`);\n return index;\n}\n\nexport function requireTrack(tracks: Track[], id: number): Track {\n const track = tracks.find((candidate) => candidate.id === id);\n if (!track) {\n const ids = tracks.map((candidate) => candidate.id);\n throw new CliError(\n `Track ${id} does not exist. Valid track IDs: ${ids.length > 0 ? ids.join(', ') : 'none'}`\n );\n }\n return track;\n}\n","import type { CommandDeps } from '../types.ts';\n\nimport { requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { probeTracks, requireTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport type EditOpts =\n | { kind: 'title'; file: string; title: string }\n | {\n kind: 'track';\n file: string;\n track: number;\n name?: string;\n language?: string;\n isDefault?: boolean;\n isForced?: boolean;\n };\n\ntype TrackEditOpts = Extract<EditOpts, { kind: 'track' }>;\n\nfunction hasTrackEdit(opts: TrackEditOpts): boolean {\n return (\n opts.name !== undefined ||\n opts.language !== undefined ||\n opts.isDefault !== undefined ||\n opts.isForced !== undefined\n );\n}\n\nexport function buildEditArgs(opts: EditOpts): string[] {\n // Node and Bun always pass argv as UTF-8 bytes; without this flag mkvpropedit\n // decodes them with the system locale and silently mangles non-ASCII values\n // (e.g. drops a Japanese title under LC_ALL=C) while still exiting 0.\n const args = ['mkvpropedit', '--command-line-charset', 'UTF-8', toolPath(opts.file)];\n switch (opts.kind) {\n case 'title': {\n args.push('--edit', 'info');\n if (opts.title === '') args.push('--delete', 'title');\n else args.push('--set', `title=${opts.title}`);\n return args;\n }\n case 'track': {\n // mkvpropedit's track:n selector is 1-based over the same track order\n // mkvmerge --identify reports, so the inspect ID maps to id + 1.\n args.push('--edit', `track:${opts.track + 1}`);\n if (opts.name === '') args.push('--delete', 'name');\n else if (opts.name !== undefined) args.push('--set', `name=${opts.name}`);\n if (opts.language !== undefined) args.push('--set', `language=${opts.language}`);\n if (opts.isDefault !== undefined)\n args.push('--set', `flag-default=${opts.isDefault ? '1' : '0'}`);\n if (opts.isForced !== undefined)\n args.push('--set', `flag-forced=${opts.isForced ? '1' : '0'}`);\n return args;\n }\n default:\n return opts satisfies never;\n }\n}\n\n/** Returns mkvpropedit's diagnostic output when it exited 1 (modified with warnings). */\nexport async function editCommand(opts: EditOpts, deps: CommandDeps): Promise<string | undefined> {\n requireInputFile(opts.file, deps.exists);\n if (opts.kind === 'track') {\n if (!hasTrackEdit(opts)) {\n throw new CliError(\n 'Nothing to edit: pass --name, --language, --default/--no-default, or --forced/--no-forced.'\n );\n }\n requireTrack(await probeTracks(deps.runner, opts.file), opts.track);\n }\n const result = await deps.runner.run(buildEditArgs(opts));\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvpropedit failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n if (result.exitCode === 1) {\n const output = [result.stdout, result.stderr]\n .map((stream) => stream.trim())\n .filter(Boolean)\n .join('\\n');\n return `mkvpropedit finished with warnings — check the file in a player:\\n${output}`;\n }\n return undefined;\n}\n","import type { CommandDeps } from '../types.ts';\n\nimport { defaultExtractOutput, ensureWritable, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport function buildExtractArgs(opts: { file: string; track: number; output: string }): string[] {\n return [\n 'mkvmerge',\n '-o',\n toolPath(opts.output),\n '--audio-tracks',\n String(opts.track),\n '--no-video',\n '--no-subtitles',\n '--no-buttons',\n '--no-chapters',\n '--no-attachments',\n '--no-global-tags',\n toolPath(opts.file)\n ];\n}\n\nexport async function extractCommand(\n opts: { file: string; track: number; output?: string; force: boolean },\n deps: CommandDeps\n): Promise<string> {\n requireInputFile(opts.file, deps.exists);\n const tracks = await probeTracks(deps.runner, opts.file);\n requireAudioTrack(tracks, opts.track);\n const output = opts.output ?? defaultExtractOutput(opts.file, opts.track);\n await ensureWritable(output, {\n force: opts.force,\n isTTY: deps.isTTY,\n confirm: deps.confirm,\n exists: deps.exists,\n inputs: [opts.file]\n });\n const result = await deps.runner.run(\n buildExtractArgs({ file: opts.file, track: opts.track, output }),\n deps.stderrIsTTY ? { stream: 'stdout' } : undefined\n );\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return output;\n}\n","import type { CommandDeps, Track } from '../types.ts';\n\nimport { requireInputFile } from '../output.ts';\nimport { probeFile } from '../probe.ts';\n\nexport function formatTable(header: readonly string[], rows: readonly string[][]): string {\n const all = [header, ...rows];\n const widths = header.map((_, column) => Math.max(...all.map((row) => row[column]!.length)));\n return all\n .map((row) =>\n row\n .map((cell, column) => cell.padEnd(widths[column]!))\n .join(' ')\n .trimEnd()\n )\n .join('\\n');\n}\n\nexport function formatTrackTable(tracks: Track[]): string {\n return formatTable(\n ['ID', 'TYPE', 'CODEC', 'LANG', 'CH', 'DEFAULT', 'FORCED', 'NAME'],\n tracks.map((track) => [\n String(track.id),\n track.type,\n track.codec,\n track.language ?? '-',\n track.channels !== undefined ? String(track.channels) : '-',\n track.isDefault ? 'yes' : '-',\n track.isForced ? 'yes' : '-',\n track.name ?? '-'\n ])\n );\n}\n\nexport async function inspectCommand(file: string, deps: CommandDeps): Promise<string> {\n requireInputFile(file, deps.exists);\n const { title, tracks } = await probeFile(deps.runner, file);\n const table = formatTrackTable(tracks);\n return title === undefined ? table : `Title: ${title}\\n\\n${table}`;\n}\n","import { existsSync } from 'node:fs';\n\nimport type { CommandDeps, Track } from '../types.ts';\n\nimport { findAliasedInput, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\nimport { formatTable } from './inspect.ts';\n\n/** One `--audio` block from the CLI: a source file plus its per-track options. */\nexport interface MuxTrackRequest {\n audio: string;\n track?: number;\n delayMs: number;\n language?: string;\n name?: string;\n makeDefault: boolean;\n}\n\nexport interface MuxOptions {\n video: string;\n tracks: readonly MuxTrackRequest[];\n output: string;\n force: boolean;\n dryRun: boolean;\n}\n\n/** A request whose track id has been resolved against the probed source. */\nexport interface ResolvedMuxTrack {\n audio: string;\n trackId: number;\n delayMs: number;\n language?: string;\n trackName?: string;\n makeDefault: boolean;\n}\n\nexport type MuxResult = { kind: 'dry-run'; plan: string } | { kind: 'written'; output: string };\n\n/** Labels one `--audio` group by position and path, e.g. `--audio #2 (\"donor.mka\")`. */\nexport function describeAudioGroup(index: number, audio: string): string {\n return `--audio #${index + 1} (\"${audio}\")`;\n}\n\nexport function resolveAudioTrackId(tracks: Track[], requested: number | undefined): number {\n if (requested !== undefined) return requireAudioTrack(tracks, requested).id;\n const audio = tracks.filter((track) => track.type === 'audio');\n if (audio.length === 1) return audio[0]!.id;\n if (audio.length === 0) throw new CliError('The audio input contains no audio tracks.');\n throw new CliError(\n `The audio input contains ${audio.length} audio tracks; pass --track <id> (see: tracksmith inspect <file>).`\n );\n}\n\nexport function buildMuxArgs(a: {\n video: string;\n videoTrackIds: readonly number[];\n clearDefaultAudioIds: readonly number[];\n groups: readonly ResolvedMuxTrack[];\n output: string;\n}): string[] {\n const trackOrder = [\n ...a.videoTrackIds.map((trackId) => `0:${trackId}`),\n ...a.groups.map((group, index) => `${index + 1}:${group.trackId}`)\n ].join(',');\n const args = [\n 'mkvmerge',\n '--command-line-charset',\n 'UTF-8',\n '-o',\n toolPath(a.output),\n '--track-order',\n trackOrder\n ];\n for (const id of a.clearDefaultAudioIds) args.push('--default-track-flag', `${id}:no`);\n args.push(toolPath(a.video));\n for (const group of a.groups) {\n const id = String(group.trackId);\n args.push(\n '--audio-tracks',\n id,\n '--no-video',\n '--no-subtitles',\n '--no-buttons',\n '--no-chapters',\n '--no-attachments',\n '--no-global-tags'\n );\n if (group.delayMs !== 0) args.push('--sync', `${id}:${group.delayMs}`);\n if (group.language !== undefined) args.push('--language', `${id}:${group.language}`);\n if (group.trackName !== undefined) args.push('--track-name', `${id}:${group.trackName}`);\n args.push('--default-track-flag', `${id}:${group.makeDefault ? 'yes' : 'no'}`);\n args.push(toolPath(group.audio));\n }\n return args;\n}\n\nfunction buildPlan(a: {\n video: string;\n videoTracks: readonly Track[];\n groups: readonly ResolvedMuxTrack[];\n sourceTracks: ReadonlyMap<string, Track[]>;\n clearDefaultAudioIds: readonly number[];\n output: string;\n}): string {\n const cleared = new Set(a.clearDefaultAudioIds);\n const existingRows = a.videoTracks.map((track) => [\n String(track.id),\n track.type,\n track.codec,\n track.language ?? '-',\n track.channels !== undefined ? String(track.channels) : '-',\n track.isDefault && !cleared.has(track.id) ? 'yes' : '-',\n '-',\n track.name ?? '-',\n a.video\n ]);\n const newRows = a.groups.map((group, index) => {\n const donor = a.sourceTracks.get(group.audio)?.find((track) => track.id === group.trackId);\n return [\n String(a.videoTracks.length + index),\n 'audio',\n donor?.codec ?? '-',\n group.language ?? donor?.language ?? '-',\n donor?.channels !== undefined ? String(donor.channels) : '-',\n group.makeDefault ? 'yes' : '-',\n group.delayMs !== 0 ? `${group.delayMs}ms` : '-',\n group.trackName ?? donor?.name ?? '-',\n group.audio\n ];\n });\n const table = formatTable(\n ['ID', 'TYPE', 'CODEC', 'LANG', 'CH', 'DEFAULT', 'DELAY', 'NAME', 'SOURCE'],\n [...existingRows, ...newRows]\n );\n return `Planned tracks for \"${a.output}\":\\n${table}`;\n}\n\nexport async function muxCommand(opts: MuxOptions, deps: CommandDeps): Promise<MuxResult> {\n requireInputFile(opts.video, deps.exists);\n const uniqueAudio = [...new Set(opts.tracks.map((track) => track.audio))];\n for (const audio of uniqueAudio) requireInputFile(audio, deps.exists);\n\n const videoTracks = await probeTracks(deps.runner, opts.video);\n const sourceTracks = new Map(\n await Promise.all(\n uniqueAudio.map(async (audio): Promise<[string, Track[]]> => [\n audio,\n await probeTracks(deps.runner, audio)\n ])\n )\n );\n\n const groups: ResolvedMuxTrack[] = opts.tracks.map((track, index) => {\n let trackId: number;\n try {\n trackId = resolveAudioTrackId(sourceTracks.get(track.audio)!, track.track);\n } catch (error) {\n if (!(error instanceof CliError)) throw error;\n throw new CliError(`${describeAudioGroup(index, track.audio)}: ${error.message}`);\n }\n return {\n audio: track.audio,\n trackId,\n delayMs: track.delayMs,\n language: track.language,\n trackName: track.name,\n makeDefault: track.makeDefault\n };\n });\n if (groups.filter((group) => group.makeDefault).length > 1) {\n throw new CliError('--default can be set on at most one --audio input.');\n }\n\n const aliasedInput = findAliasedInput(opts.output, [opts.video, ...uniqueAudio]);\n if (aliasedInput !== undefined) {\n throw new CliError(\n `Output \"${opts.output}\" is the same file as input \"${aliasedInput}\". Choose a different output path.`\n );\n }\n\n const clearDefaultAudioIds = groups.some((group) => group.makeDefault)\n ? videoTracks\n .filter((track) => track.type === 'audio' && track.isDefault)\n .map((track) => track.id)\n : [];\n const plan = buildPlan({\n video: opts.video,\n videoTracks,\n groups,\n sourceTracks,\n clearDefaultAudioIds,\n output: opts.output\n });\n if (opts.dryRun) return { kind: 'dry-run', plan };\n\n const exists = deps.exists ?? existsSync;\n const outputExists = exists(opts.output);\n if (!opts.force) {\n if (deps.isTTY) {\n const overwrite = outputExists\n ? `\\nOutput \"${opts.output}\" already exists and will be overwritten.`\n : '';\n const approved = await deps.confirm(`${plan}${overwrite}\\nWrite \"${opts.output}\"?`);\n if (!approved) throw new CliError('Aborted: nothing written.');\n } else if (outputExists) {\n throw new CliError(\n `Output \"${opts.output}\" already exists. Pass --force to overwrite (cannot prompt in a non-interactive session).`\n );\n }\n }\n\n const result = await deps.runner.run(\n buildMuxArgs({\n video: opts.video,\n videoTrackIds: videoTracks.map((track) => track.id),\n clearDefaultAudioIds,\n groups,\n output: opts.output\n }),\n deps.stderrIsTTY ? { stream: 'stdout' } : undefined\n );\n if (result.exitCode >= 2) {\n throw new CliError(\n `mkvmerge failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return { kind: 'written', output: opts.output };\n}\n","import type { CommandDeps } from '../types.ts';\n\nimport { defaultTestClipOutput, ensureWritable, requireInputFile } from '../output.ts';\nimport { toolPath } from '../paths.ts';\nimport { audioRelativeIndex, probeTracks, requireAudioTrack } from '../probe.ts';\nimport { CliError } from '../types.ts';\n\nexport function parseTimeToSeconds(value: string): number {\n if (/^\\d+(\\.\\d+)?$/.test(value)) return Number(value);\n const match = /^(\\d+):([0-5]?\\d):([0-5]?\\d(?:\\.\\d+)?)$/.exec(value);\n if (!match)\n throw new CliError(`Invalid time \"${value}\". Use seconds (90 or 90.5) or HH:MM:SS[.ms].`);\n return Number(match[1]) * 3600 + Number(match[2]) * 60 + Number(match[3]);\n}\n\nexport function buildTestClipArgs(a: {\n video: string;\n audio: string;\n audioStreamIndex: number;\n startSeconds: number;\n durationSeconds: number;\n delayMs: number;\n output: string;\n}): string[] {\n // -y is safe: our own overwrite policy (ensureWritable) has already run.\n return [\n 'ffmpeg',\n '-hide_banner',\n '-nostdin',\n '-ss',\n String(a.startSeconds),\n '-i',\n toolPath(a.video),\n '-itsoffset',\n String(a.delayMs / 1000),\n '-ss',\n String(a.startSeconds),\n '-i',\n toolPath(a.audio),\n '-map',\n '0:v:0',\n '-map',\n `1:a:${a.audioStreamIndex}`,\n '-t',\n String(a.durationSeconds),\n '-c:v',\n 'libx264',\n '-preset',\n 'veryfast',\n '-crf',\n '18',\n '-c:a',\n 'copy',\n '-sn',\n '-abort_on',\n 'empty_output_stream',\n '-y',\n toolPath(a.output)\n ];\n}\n\nexport async function testClipCommand(\n opts: {\n video: string;\n audio: string;\n track: number;\n start: string;\n duration: string;\n delayMs: number;\n output?: string;\n force: boolean;\n },\n deps: CommandDeps\n): Promise<string> {\n requireInputFile(opts.video, deps.exists);\n requireInputFile(opts.audio, deps.exists);\n const startSeconds = parseTimeToSeconds(opts.start);\n const durationSeconds = parseTimeToSeconds(opts.duration);\n if (durationSeconds <= 0) throw new CliError('--duration must be greater than zero.');\n const donorTracks = await probeTracks(deps.runner, opts.audio);\n requireAudioTrack(donorTracks, opts.track);\n const output = opts.output ?? defaultTestClipOutput(opts.video);\n await ensureWritable(output, {\n force: opts.force,\n isTTY: deps.isTTY,\n confirm: deps.confirm,\n exists: deps.exists,\n inputs: [opts.video, opts.audio]\n });\n const result = await deps.runner.run(\n buildTestClipArgs({\n video: opts.video,\n audio: opts.audio,\n audioStreamIndex: audioRelativeIndex(donorTracks, opts.track),\n startSeconds,\n durationSeconds,\n delayMs: opts.delayMs,\n output\n }),\n deps.stderrIsTTY ? { stream: 'stderr' } : undefined\n );\n if (result.exitCode !== 0) {\n throw new CliError(\n `ffmpeg failed (exit ${result.exitCode}):\\n${result.stderr || result.stdout}`\n );\n }\n return output;\n}\n","import { parseArgs } from 'node:util';\n\nimport type { CommandDeps } from './types.ts';\n\nimport { editCommand } from './commands/edit.ts';\nimport { extractCommand } from './commands/extract.ts';\nimport { inspectCommand } from './commands/inspect.ts';\nimport { describeAudioGroup, muxCommand } from './commands/mux.ts';\nimport { testClipCommand } from './commands/test-clip.ts';\nimport { CliError } from './types.ts';\n\nconst USAGE = `tracksmith — inspect, edit, extract, sync-test, and mux Matroska audio tracks\n\nUsage:\n tracksmith inspect <file>\n tracksmith extract <file> --track <id> [--output <file>] [--force]\n tracksmith edit <file> --track <id> [--name <text>] [--language <lang>]\n [--default | --no-default] [--forced | --no-forced]\n tracksmith edit <file> --title <text>\n tracksmith test-clip --video <target> --audio <donor> --track <id> --start <time>\n [--duration 60] [--delay-ms 0] [--output <file>] [--force]\n tracksmith mux --video <target> --output <file> [--dry-run] [--force]\n --audio <mka-or-mkv> [--track <id>] [--delay-ms 0] [--language eng] [--name <title>] [--default]\n [--audio <next> ...]\n\nNotes:\n Repeat --audio to add several tracks; per-track flags apply to the most recent --audio.\n --default may be set on at most one --audio; it also clears the target's existing default audio flags.\n --dry-run prints the planned track layout without writing.\n --track is always the MKVToolNix track ID shown by \"tracksmith inspect\".\n edit changes metadata in place without remuxing. --name \"\" and --title \"\" clear the value\n (in Windows PowerShell use --name= and --title=; empty \"\" arguments get dropped).\n --title sets the file title and cannot be combined with --track edits.\n Positive --delay-ms delays audio; negative advances it (e.g. --delay-ms -250).\n Times accept seconds (90, 90.5) or HH:MM:SS[.ms].\n Check sync near the beginning, middle, and end; a changing offset cannot be fixed by one delay.\n Requires ffmpeg and MKVToolNix (mkvmerge, mkvpropedit) on PATH.`;\n\nfunction parseIntStrict(value: string | undefined, flag: string): number {\n if (value === undefined) throw new CliError(`${flag} is required.`);\n if (!/^-?\\d+$/.test(value)) throw new CliError(`${flag} must be an integer, got \"${value}\".`);\n return Number(value);\n}\n\nfunction parseTrack(value: string | undefined): number {\n const track = parseIntStrict(value, '--track');\n if (track < 0) throw new CliError(`--track must be >= 0, got ${track}.`);\n return track;\n}\n\nfunction requireFilePositional(\n positionals: string[],\n command: 'inspect' | 'extract' | 'edit'\n): string {\n if (positionals.length === 0) throw new CliError(`${command} requires a file argument.`);\n if (positionals.length > 1) throw new CliError(`${command} accepts exactly one file argument.`);\n return positionals[0]!;\n}\n\n// Wraps a parseArgs call so parse failures become CliErrors. Takes a thunk\n// (rather than the options object) so parseArgs keeps its precise generic\n// inference over each command's inline options literal.\nfunction parseOrCliError<T>(parseFn: () => T): T {\n try {\n return parseFn();\n } catch (error) {\n throw new CliError(error instanceof Error ? error.message : String(error));\n }\n}\n\n// parseArgs treats a leading-dash value token (\"-250\") as an option, so fold\n// `--delay-ms -250` into the equals form it does accept.\nfunction normalizeDelayMs(args: string[]): string[] {\n const out: string[] = [];\n for (let i = 0; i < args.length; i += 1) {\n const arg = args[i]!;\n const next = args[i + 1];\n if (arg === '--delay-ms' && next !== undefined && /^-\\d+$/.test(next)) {\n out.push(`--delay-ms=${next}`);\n i += 1;\n } else {\n out.push(arg);\n }\n }\n return out;\n}\n\nexport async function runCli(\n argv: string[],\n deps: CommandDeps,\n stdout: (line: string) => void = console.log\n): Promise<number> {\n const [command, ...rawRest] = argv;\n const rest = normalizeDelayMs(rawRest);\n try {\n switch (command) {\n case undefined: {\n stdout(USAGE);\n return 1;\n }\n case '--help':\n case '-h': {\n stdout(USAGE);\n return 0;\n }\n case 'inspect': {\n const { positionals } = parseOrCliError(() =>\n parseArgs({ args: rest, options: {}, allowPositionals: true, strict: true })\n );\n const file = requireFilePositional(positionals, 'inspect');\n stdout(await inspectCommand(file, deps));\n return 0;\n }\n case 'extract': {\n const { values, positionals } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n track: { type: 'string' },\n output: { type: 'string' },\n force: { type: 'boolean', default: false }\n },\n allowPositionals: true,\n strict: true\n })\n );\n const file = requireFilePositional(positionals, 'extract');\n const output = await extractCommand(\n {\n file,\n track: parseTrack(values.track),\n output: values.output,\n force: values.force ?? false\n },\n deps\n );\n stdout(`Wrote ${output}`);\n return 0;\n }\n case 'test-clip': {\n const { values } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n video: { type: 'string' },\n audio: { type: 'string' },\n track: { type: 'string' },\n start: { type: 'string' },\n duration: { type: 'string', default: '60' },\n 'delay-ms': { type: 'string', default: '0' },\n output: { type: 'string' },\n force: { type: 'boolean', default: false }\n },\n strict: true\n })\n );\n if (!values.video || !values.audio)\n throw new CliError('test-clip requires --video and --audio.');\n if (!values.start) throw new CliError('test-clip requires --start.');\n const output = await testClipCommand(\n {\n video: values.video,\n audio: values.audio,\n track: parseTrack(values.track),\n start: values.start,\n duration: values.duration ?? '60',\n delayMs: parseIntStrict(values['delay-ms'] ?? '0', '--delay-ms'),\n output: values.output,\n force: values.force ?? false\n },\n deps\n );\n stdout(`Wrote ${output}`);\n return 0;\n }\n case 'mux': {\n const { values, tokens } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n video: { type: 'string' },\n output: { type: 'string' },\n force: { type: 'boolean', default: false },\n 'dry-run': { type: 'boolean', default: false },\n audio: { type: 'string', multiple: true },\n track: { type: 'string', multiple: true },\n 'delay-ms': { type: 'string', multiple: true },\n language: { type: 'string', multiple: true },\n name: { type: 'string', multiple: true },\n default: { type: 'boolean', multiple: true }\n },\n strict: true,\n tokens: true\n })\n );\n const groups: {\n audio: string;\n track?: string;\n delayMs?: string;\n language?: string;\n name?: string;\n makeDefault?: true;\n }[] = [];\n for (const token of tokens) {\n if (token.kind !== 'option') continue;\n if (token.name === 'audio') {\n groups.push({ audio: token.value });\n continue;\n }\n if (\n token.name !== 'track' &&\n token.name !== 'delay-ms' &&\n token.name !== 'language' &&\n token.name !== 'name' &&\n token.name !== 'default'\n ) {\n continue;\n }\n const group = groups.at(-1);\n if (group === undefined) {\n throw new CliError(`--${token.name} must come after an --audio input.`);\n }\n switch (token.name) {\n case 'track':\n if (group.track !== undefined)\n throw new CliError(\n `Duplicate --track for ${describeAudioGroup(groups.length - 1, group.audio)}.`\n );\n group.track = token.value;\n break;\n case 'delay-ms':\n if (group.delayMs !== undefined)\n throw new CliError(\n `Duplicate --delay-ms for ${describeAudioGroup(groups.length - 1, group.audio)}.`\n );\n group.delayMs = token.value;\n break;\n case 'language':\n if (group.language !== undefined)\n throw new CliError(\n `Duplicate --language for ${describeAudioGroup(groups.length - 1, group.audio)}.`\n );\n group.language = token.value;\n break;\n case 'name':\n if (group.name !== undefined)\n throw new CliError(\n `Duplicate --name for ${describeAudioGroup(groups.length - 1, group.audio)}.`\n );\n group.name = token.value;\n break;\n case 'default':\n if (group.makeDefault !== undefined)\n throw new CliError(\n `Duplicate --default for ${describeAudioGroup(groups.length - 1, group.audio)}.`\n );\n group.makeDefault = true;\n break;\n }\n }\n if (!values.video || groups.length === 0)\n throw new CliError('mux requires --video and --audio.');\n if (!values.output)\n throw new CliError('mux requires --output (no default output name for the final file).');\n const result = await muxCommand(\n {\n video: values.video,\n tracks: groups.map((group) => ({\n audio: group.audio,\n track: group.track === undefined ? undefined : parseTrack(group.track),\n delayMs: parseIntStrict(group.delayMs ?? '0', '--delay-ms'),\n language: group.language ?? 'eng',\n name: group.name,\n makeDefault: group.makeDefault ?? false\n })),\n output: values.output,\n force: values.force ?? false,\n dryRun: values['dry-run'] ?? false\n },\n deps\n );\n if (result.kind === 'dry-run') stdout(result.plan);\n else stdout(`Wrote ${result.output}`);\n return 0;\n }\n case 'edit': {\n const { values, positionals } = parseOrCliError(() =>\n parseArgs({\n args: rest,\n options: {\n track: { type: 'string' },\n name: { type: 'string' },\n language: { type: 'string' },\n default: { type: 'boolean' },\n 'no-default': { type: 'boolean' },\n forced: { type: 'boolean' },\n 'no-forced': { type: 'boolean' },\n title: { type: 'string' }\n },\n allowPositionals: true,\n strict: true\n })\n );\n const file = requireFilePositional(positionals, 'edit');\n const hasTrackFlags =\n values.track !== undefined ||\n values.name !== undefined ||\n values.language !== undefined ||\n values.default !== undefined ||\n values['no-default'] !== undefined ||\n values.forced !== undefined ||\n values['no-forced'] !== undefined;\n if (values.title !== undefined && hasTrackFlags)\n throw new CliError(\n '--title cannot be combined with --track edits. Run them as separate invocations.'\n );\n if (values.default !== undefined && values['no-default'] !== undefined)\n throw new CliError('Pass either --default or --no-default, not both.');\n if (values.forced !== undefined && values['no-forced'] !== undefined)\n throw new CliError('Pass either --forced or --no-forced, not both.');\n if (values.language === '')\n throw new CliError('--language cannot be empty. Use --language und for undetermined.');\n let warning: string | undefined;\n if (values.title !== undefined) {\n warning = await editCommand({ kind: 'title', file, title: values.title }, deps);\n } else {\n if (values.track === undefined)\n throw new CliError('edit requires --track (or --title for the file title).');\n warning = await editCommand(\n {\n kind: 'track',\n file,\n track: parseTrack(values.track),\n name: values.name,\n language: values.language,\n isDefault: values.default ?? (values['no-default'] !== undefined ? false : undefined),\n isForced: values.forced ?? (values['no-forced'] !== undefined ? false : undefined)\n },\n deps\n );\n }\n if (warning !== undefined) console.error(`tracksmith: ${warning}`);\n stdout(`Edited ${file}`);\n return 0;\n }\n default:\n throw new CliError(`Unknown command \"${command}\".\\n\\n${USAGE}`);\n }\n } catch (error) {\n if (error instanceof CliError) {\n console.error(`tracksmith: ${error.message}`);\n return error.exitCode;\n }\n throw error;\n }\n}\n","import { once } from 'node:events';\nimport { createInterface } from 'node:readline/promises';\n\nimport type { Readable, Writable } from 'node:stream';\n\nexport interface ConfirmPromptOptions {\n input?: Readable;\n output?: Writable;\n}\n\nexport function createConfirmPrompt(\n options: ConfirmPromptOptions = {}\n): (message: string) => Promise<boolean> {\n const input = options.input ?? process.stdin;\n const output = options.output ?? process.stdout;\n\n return async (message: string): Promise<boolean> => {\n const readline = createInterface({ input, output });\n\n try {\n const closed = once(readline, 'close').then(() => ({ kind: 'eof' }) as const);\n const answered = readline.question(`${message} [y/N] `).then(\n (value) => ({ kind: 'answer', value }) as const,\n () => ({ kind: 'eof' }) as const\n );\n const result = await Promise.race([answered, closed]);\n\n return result.kind === 'answer' && /^y(?:es)?$/i.test(result.value.trim());\n } finally {\n readline.close();\n }\n };\n}\n","export const TOOLS = ['ffmpeg', 'mkvmerge', 'mkvpropedit'] as const;\n\nexport type ToolName = (typeof TOOLS)[number];\n\nexport function isToolName(value: string): value is ToolName {\n return TOOLS.some((tool) => tool === value);\n}\n\nconst MKVTOOLNIX_HINT =\n 'Windows: winget install MoritzBunkus.MKVToolNix or scoop install mkvtoolnix | macOS: brew install mkvtoolnix | Linux: sudo apt install mkvtoolnix or sudo pacman -S mkvtoolnix-cli';\n\nconst INSTALL_HINTS = {\n ffmpeg:\n 'Windows: winget install Gyan.FFmpeg or scoop install ffmpeg | macOS: brew install ffmpeg | Linux: sudo apt install ffmpeg or sudo pacman -S ffmpeg',\n mkvmerge: MKVTOOLNIX_HINT,\n mkvpropedit: MKVTOOLNIX_HINT\n} satisfies Record<ToolName, string>;\n\nexport function installHint(tool: ToolName): string {\n return INSTALL_HINTS[tool];\n}\n","import { spawn } from 'node:child_process';\n\nimport type { RunOptions, Runner, RunResult } from './types.ts';\n\nimport { installHint, isToolName } from './tools.ts';\nimport { CliError } from './types.ts';\n\nexport function processStartError(program: string, error: unknown): CliError {\n if (\n typeof error === 'object' &&\n error !== null &&\n 'code' in error &&\n error.code === 'ENOENT' &&\n isToolName(program)\n ) {\n return new CliError(\n `${program} is required but was not found on PATH. ${installHint(program)}`\n );\n }\n\n const message = error instanceof Error ? error.message : String(error);\n return new CliError(`Failed to start ${program}: ${message}`);\n}\n\nconst writeToCliStderr = (chunk: string): void => {\n globalThis.process.stderr.write(chunk);\n};\n\nexport class ProcessRunner implements Runner {\n private readonly sink: (chunk: string) => void;\n\n constructor(options: { sink?: (chunk: string) => void } = {}) {\n this.sink = options.sink ?? writeToCliStderr;\n }\n\n async run(argv: readonly string[], options?: RunOptions): Promise<RunResult> {\n const [executable, ...args] = argv;\n if (executable === undefined) {\n throw new CliError('Could not start process: missing executable');\n }\n\n return await new Promise((resolve, reject) => {\n const process = spawn(executable, args, {\n stdio: ['ignore', 'pipe', 'pipe']\n });\n let stdout = '';\n let stderr = '';\n\n process.stdout.setEncoding('utf8');\n process.stderr.setEncoding('utf8');\n process.stdout.on('data', (chunk: string) => {\n stdout += chunk;\n if (options?.stream === 'stdout') this.sink(chunk);\n });\n process.stderr.on('data', (chunk: string) => {\n stderr += chunk;\n if (options?.stream === 'stderr') this.sink(chunk);\n });\n process.once('error', (error) => {\n reject(processStartError(executable, error));\n });\n // Windows loses the signal: a killed child reports a plain numeric exit\n // code (usually 1) with signal null, so this detection is POSIX-only and\n // Windows cannot distinguish a killed tool from a warning exit.\n process.once('close', (exitCode, signal) => {\n if (signal !== null) {\n const output = [stdout, stderr]\n .map((stream) => stream.trim())\n .filter(Boolean)\n .join('\\n');\n const diagnostics = output === '' ? '' : `\\nOutput before termination:\\n${output}`;\n reject(new CliError(`${executable} was terminated by signal ${signal}.${diagnostics}`));\n return;\n }\n resolve({ exitCode: exitCode ?? 1, stdout, stderr });\n });\n });\n }\n}\n","#!/usr/bin/env node\n\nimport { runCli } from './cli.ts';\nimport { createConfirmPrompt } from './prompt.ts';\nimport { ProcessRunner } from './runner.ts';\n\nprocess.exitCode = await runCli(process.argv.slice(2), {\n runner: new ProcessRunner(),\n isTTY: process.stdin.isTTY ?? false,\n stderrIsTTY: process.stderr.isTTY ?? false,\n confirm: createConfirmPrompt({ output: process.stderr })\n});\n"],"mappings":"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"}
|