pwebm 0.0.1 → 0.1.0-alpha.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 CHANGED
@@ -27,7 +27,7 @@ bun i -g git+https://github.com/4ndrs/pwebm.git
27
27
 
28
28
  ## Historical Background & Purpose
29
29
 
30
- In 2022, I wrote a script for the mpv media player called [PureMPV](https://github.com/4ndrs/PureMPV) that would help extracting the currently watched video's information, like the timestamps at certain points, and the the cropping coordinates, which would then later be used for encoding videos with ffmpeg with specific parameters.
30
+ In 2022, I wrote a script for the mpv media player called [PureMPV](https://github.com/4ndrs/PureMPV) that would help extracting the currently watched video's information, like the timestamps at certain points, and the cropping coordinates, which would then later be used for encoding videos with ffmpeg with specific parameters.
31
31
 
32
32
  Up to that point, I mostly spent the time encoding short webm files to keep and share on some imageboards, as well as encoding some mkv files with streams and attachments copied, so I wanted to integrate this functionality in some way with PureMPV. I didn't like the idea of having PureMPV to do the encoding itself, I wanted a seperate process to handle this independently, so I could stop or start another mpv window without affecting the encoding process. I wrote down some of the initial requirements at the time, which were the following:
33
33
 
package/TODO.md CHANGED
@@ -1,30 +1,16 @@
1
- ## Stuff
2
-
3
- - [x] argument parser
4
- - [x] expand tilde and $HOME in config video path
5
- - [x] check for ffmpeg and ffprobe in path
6
- - [x] convert webm, retry when limit reached
7
- - [x] convert any other encoder to mkv copied streams just like purewebm
8
- - [x] socket file for communication
9
- - [x] handle queue
10
- - [x] kill implementation
11
- - [x] ffmpeg progress
12
- - [x] status implementation
13
- - [x] handle signals
14
- - [x] name the process
15
- - [x] version implementation
16
- - [x] help implementation
17
- - [x] args.ts help is missing dynamic default for crf
18
- - [x] if input is missing after some other arg, the default parse error is shown
19
- - [x] warn color is messed up for the message shown when the file limit is reached because of default warn color
20
- - [x] implement better cleaning process before exit
21
- - [x] release tagged versions in npm
22
- - [x] it's not part of this repo, but don't forget to improve the helper script for PureMPV (pwebm-helper), it has a missing readme
23
- - [x] update readme
24
- - [x] add subs flag warning to the readme (how it works, output seeking needed, etc)
1
+ ## 0.1.0
2
+ - [x] (pwebm-helper) add a keybinding to show the status of the encoding on the mpv window
3
+ - [x] no log file flag
4
+ - [ ] add no log file flag to the docs
5
+ - [ ] when erroring out encoding an item, just log the error and move on to the next item (save the exit code)
6
+ - [ ] update the status on the terminal right away when adding a new item to the queue
7
+ - [ ] rename tries prop in status to try
8
+ - [ ] document status in the readme
9
+ - [ ] pass file to select in the -subs option
10
+ - [ ] pass false to -subs to disable subtitles (if enabled in the config)
25
11
 
26
12
  ### Might be nice to have
27
13
  - [ ] add limit for the amount of tries to redo a conversion
28
14
  - [ ] percentage bitrate offset when retrying a conversion with new calcs
29
15
  - [ ] handle no duration case, maybe adding an additional crf mode instead of bitrate calcs
30
- - [ ] (helper script) add a keybinding to show the status of the encoding on the mpv window
16
+ - [ ] multiple encodings at the same time
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pwebm",
3
- "version": "0.0.1",
3
+ "version": "0.1.0-alpha.0",
4
4
  "author": "4ndrs <andres.degozaru@gmail.com>",
5
5
  "repository": {
6
6
  "type": "git",
package/src/args.ts CHANGED
@@ -8,9 +8,8 @@ import {
8
8
  COPYRIGHT_YEAR,
9
9
  } from "./constants";
10
10
 
11
- import { ipc } from "./ipc";
12
11
  import { config } from "./config";
13
- import { logger } from "./logger";
12
+ import { ipcClient } from "./ipc/client";
14
13
  import { ArgsSchema } from "./schema/args";
15
14
 
16
15
  const RECOGNIZED_ARGS = [
@@ -34,412 +33,411 @@ const RECOGNIZED_ARGS = [
34
33
  "-ep",
35
34
  "--extra-params",
36
35
  "--video-path",
36
+ "--no-log-file",
37
37
  ];
38
38
 
39
- export const parseArgs = async (args: string[]): Promise<ArgsSchema> => {
40
- const rawArgs: Partial<ArgsSchema> = {};
39
+ const printUsage = () => {
40
+ const usage = `Usage: ${CLI_NAME} [options] [[infile options] -i infile]... [outfile options] [outfile] [extra params]
41
41
 
42
- let skip = false;
43
- let isExtraParam = false;
44
- let seeking: { startTime?: string; stopTime?: string } | undefined;
42
+ ${DESCRIPTION}
45
43
 
46
- const skipNext = () => (skip = true);
44
+ Positional arguments:
45
+ [outfile] The output file. If not specified, a generated unix timestamp as filename will be used
46
+ and saved to the directory set in the --video-path option
47
47
 
48
- for (let index = 0; index < args.length; index++) {
49
- const arg = args[index];
48
+ Options:
49
+ -h, --help Show this help message
50
+ -v, --version Show version information
51
+ -kill Terminate the running ${CLI_NAME} instance, if there is any
52
+ -status Show the current status
53
+ -i <input> The input file to encode
54
+ -ss <start_time> The start time (same as ffmpeg's -ss)
55
+ -to <stop_time> The stop time (same as ffmpeg's -to)
56
+ -lavfi <filters> The set of filters to pass to ffmpeg
57
+ -c:v <encoder> The video encoder to use (default is ${config.encoder})
58
+ -deadline {good,best} The deadline for libvpx-vp9; good is the recommended one, best has the best
59
+ compression efficiency but takes the most time (default is ${config.deadline})
60
+ -crf <value> The crf to use (default is ${config.crf})
61
+ -cpu-used {0,1,2,3,4,5} The cpu-used for libvpx-vp9; a number between 0 and 5 inclusive, the higher
62
+ the number the faster the encoding will be with a quality trade-off (default is ${config.cpuUsed})
63
+ -subs Burn the subtitles onto the output file; this flag will automatically use
64
+ the subtitles found in the first input file, to use a different file use
65
+ the -lavfi flag with the subtitles filter directly
66
+ -sl, --size-limit <limit> The size limit of the output file in MiB, use 0 for no limit (default is ${config.sizeLimit})
67
+ --video-path <path> The video path where the video files are stored (default is ${config.videoPath})
68
+ this is overridden if the output file is specified
69
+ --no-log-file Don't log to the log file
70
+ -ep, --extra-params <params>
71
+ The extra parameters to pass to ffmpeg, these will be appended making it
72
+ possible to override some defaults. This option has to be the last one, everything
73
+ will be passed as is to ffmpeg
50
74
 
51
- if (skip) {
52
- skip = false;
53
- continue;
54
- }
75
+ Examples:
76
+ ${CLI_NAME} -i "/tmp/Videos/nijinosaki.mkv" -ss 00:00:02.268 -to 00:00:10.310
77
+ ${CLI_NAME} -i "/tmp/Videos/nijinosaki.mkv" --size-limit 6 -subs --extra-params -map 0:a -c:a libopus -b:a 128k`;
55
78
 
56
- if (isExtraParam) {
57
- rawArgs.extraParams?.push(arg);
58
- continue;
59
- }
79
+ console.info(usage);
80
+ };
60
81
 
61
- if (arg.startsWith("-") && !RECOGNIZED_ARGS.includes(arg)) {
62
- printUsage();
82
+ const logMissingArg = (arg: string) =>
83
+ console.error(`The ${arg} flag requires an argument`);
63
84
 
64
- logger.error(`Unrecognized argument: ${arg}`);
85
+ const logInvalidNumber = (arg: string, value: number) =>
86
+ console.error(
87
+ `The ${arg} flag requires a number. "${value}" is not a valid number`,
88
+ );
65
89
 
66
- process.exit(1);
67
- }
90
+ const argv = Bun.argv.slice(2);
68
91
 
69
- if (["-h", "--help"].includes(arg)) {
70
- printUsage();
92
+ const rawArgs: Partial<ArgsSchema> = {};
71
93
 
72
- process.exit();
73
- }
94
+ let skip = false;
95
+ let isExtraParam = false;
96
+ let seeking: { startTime?: string; stopTime?: string } | undefined;
74
97
 
75
- if (["-v", "--version"].includes(arg)) {
76
- logger.info(
77
- `${CLI_NAME} version ${VERSION}\nCopyright (c) ${COPYRIGHT_YEAR} ${AUTHOR}\nLicensed under the ${LICENSE} License\n${HOMEPAGE}`,
78
- { onlyConsole: true },
79
- );
80
- process.exit();
81
- }
98
+ const skipNext = () => (skip = true);
82
99
 
83
- if (arg === "-kill") {
84
- try {
85
- await ipc.sendMessage({ type: "kill" });
86
-
87
- logger.info("Main instance successfully killed", {
88
- logToConsole: true,
89
- });
90
- } catch (error) {
91
- if (
92
- error instanceof Error &&
93
- "code" in error &&
94
- error.code !== "ENOENT"
95
- ) {
96
- logger.error("Couldn't kill the main instance");
97
- }
98
-
99
- process.exit(1);
100
- }
100
+ for (let index = 0; index < argv.length; index++) {
101
+ const arg = argv[index];
101
102
 
102
- process.exit();
103
- }
104
-
105
- if (arg === "-status") {
106
- try {
107
- const status = await ipc.sendMessage({ type: "status" });
103
+ if (skip) {
104
+ skip = false;
105
+ continue;
106
+ }
108
107
 
109
- logger.info(JSON.stringify(status, null, 2), { onlyConsole: true });
108
+ if (isExtraParam) {
109
+ rawArgs.extraParams?.push(arg);
110
+ continue;
111
+ }
110
112
 
111
- logger.info("Status printed to the screen");
112
- } catch (error) {
113
- if (
114
- error instanceof Error &&
115
- "code" in error &&
116
- error.code !== "ENOENT"
117
- ) {
118
- logger.error("Couldn't get the status of the main instance");
119
- }
113
+ if (arg.startsWith("-") && !RECOGNIZED_ARGS.includes(arg)) {
114
+ printUsage();
120
115
 
121
- process.exit(1);
122
- }
116
+ console.error(`Unrecognized argument: ${arg}`);
123
117
 
124
- process.exit();
125
- }
118
+ process.exit(1);
119
+ }
126
120
 
127
- if (!arg.startsWith("-") && !arg.startsWith("--")) {
128
- if (rawArgs.output?.file) {
129
- logger.error("Only one output file is allowed");
121
+ if (["-h", "--help"].includes(arg)) {
122
+ printUsage();
130
123
 
131
- logger.debug(
132
- `Current output file: ${rawArgs.output.file}, new file: ${arg}`,
133
- );
124
+ process.exit();
125
+ }
134
126
 
135
- process.exit(1);
136
- }
127
+ if (["-v", "--version"].includes(arg)) {
128
+ console.info(
129
+ `${CLI_NAME} version ${VERSION}\nCopyright (c) ${COPYRIGHT_YEAR} ${AUTHOR}\nLicensed under the ${LICENSE} License\n${HOMEPAGE}`,
130
+ );
131
+ process.exit();
132
+ }
137
133
 
138
- if (!rawArgs.output) {
139
- rawArgs.output = {};
134
+ if (arg === "-kill") {
135
+ try {
136
+ await ipcClient.sendMessage({ type: "kill" });
137
+
138
+ console.info("Main instance successfully killed");
139
+ } catch (error) {
140
+ if (
141
+ error instanceof Error &&
142
+ "code" in error &&
143
+ error.code !== "ENOENT"
144
+ ) {
145
+ console.error("Couldn't kill the main instance");
140
146
  }
141
147
 
142
- rawArgs.output.file = arg;
143
-
144
- continue;
148
+ process.exit(1);
145
149
  }
146
150
 
147
- if (arg === "-ss") {
148
- if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
149
- logMissingArg(arg);
151
+ process.exit();
152
+ }
150
153
 
151
- process.exit(1);
154
+ if (arg === "-status") {
155
+ try {
156
+ const status = await ipcClient.sendMessage({ type: "status" });
157
+
158
+ console.info(JSON.stringify(status, null, 2));
159
+ } catch (error) {
160
+ if (
161
+ error instanceof Error &&
162
+ "code" in error &&
163
+ error.code !== "ENOENT"
164
+ ) {
165
+ console.error("Couldn't get the status of the main instance");
152
166
  }
153
167
 
154
- if (!seeking) {
155
- seeking = {};
156
- }
168
+ process.exit(1);
169
+ }
157
170
 
158
- seeking.startTime = args[index + 1];
171
+ process.exit();
172
+ }
159
173
 
160
- skipNext();
174
+ if (!arg.startsWith("-") && !arg.startsWith("--")) {
175
+ if (rawArgs.output?.file) {
176
+ console.error("Only one output file is allowed");
161
177
 
162
- continue;
163
- }
178
+ console.debug(
179
+ `Current output file: ${rawArgs.output.file}, new file: ${arg}`,
180
+ );
164
181
 
165
- if (arg === "-to") {
166
- if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
167
- logMissingArg(arg);
182
+ process.exit(1);
183
+ }
168
184
 
169
- process.exit(1);
170
- }
185
+ if (!rawArgs.output) {
186
+ rawArgs.output = {};
187
+ }
171
188
 
172
- if (!seeking) {
173
- seeking = {};
174
- }
189
+ rawArgs.output.file = arg;
175
190
 
176
- seeking.stopTime = args[index + 1];
191
+ continue;
192
+ }
177
193
 
178
- skipNext();
194
+ if (arg === "-ss") {
195
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith("-")) {
196
+ logMissingArg(arg);
179
197
 
180
- continue;
198
+ process.exit(1);
181
199
  }
182
200
 
183
- if (arg === "-i") {
184
- if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
185
- logMissingArg(arg);
186
-
187
- process.exit(1);
188
- }
201
+ if (!seeking) {
202
+ seeking = {};
203
+ }
189
204
 
190
- if (!rawArgs.inputs) {
191
- rawArgs.inputs = [];
192
- }
205
+ seeking.startTime = argv[index + 1];
193
206
 
194
- rawArgs.inputs.push({
195
- file: args[index + 1],
196
- ...seeking,
197
- });
207
+ skipNext();
198
208
 
199
- seeking = undefined;
209
+ continue;
210
+ }
200
211
 
201
- skipNext();
212
+ if (arg === "-to") {
213
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith("-")) {
214
+ logMissingArg(arg);
202
215
 
203
- continue;
216
+ process.exit(1);
204
217
  }
205
218
 
206
- if (arg === "-subs") {
207
- rawArgs.subs = true;
208
-
209
- continue;
219
+ if (!seeking) {
220
+ seeking = {};
210
221
  }
211
222
 
212
- if (arg === "-sl" || arg === "--size-limit") {
213
- if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
214
- logMissingArg(arg);
223
+ seeking.stopTime = argv[index + 1];
215
224
 
216
- process.exit(1);
217
- }
225
+ skipNext();
218
226
 
219
- const sizeLimit = Number(args[index + 1]);
227
+ continue;
228
+ }
220
229
 
221
- if (isNaN(sizeLimit)) {
222
- logInvalidNumber(arg, sizeLimit);
230
+ if (arg === "-i") {
231
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith("-")) {
232
+ logMissingArg(arg);
223
233
 
224
- process.exit(1);
225
- }
234
+ process.exit(1);
235
+ }
226
236
 
227
- rawArgs.sizeLimit = sizeLimit;
237
+ if (!rawArgs.inputs) {
238
+ rawArgs.inputs = [];
239
+ }
228
240
 
229
- skipNext();
241
+ rawArgs.inputs.push({
242
+ file: argv[index + 1],
243
+ ...seeking,
244
+ });
230
245
 
231
- continue;
232
- }
246
+ seeking = undefined;
233
247
 
234
- if (arg === "-ep" || arg === "--extra-params") {
235
- if (args[index + 1] === undefined) {
236
- logMissingArg(arg);
248
+ skipNext();
237
249
 
238
- process.exit(1);
239
- }
250
+ continue;
251
+ }
240
252
 
241
- isExtraParam = true;
253
+ if (arg === "-subs") {
254
+ rawArgs.subs = true;
242
255
 
243
- rawArgs.extraParams = [];
256
+ continue;
257
+ }
258
+
259
+ if (arg === "-sl" || arg === "--size-limit") {
260
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith("-")) {
261
+ logMissingArg(arg);
244
262
 
245
- continue;
263
+ process.exit(1);
246
264
  }
247
265
 
248
- if (arg === "--video-path") {
249
- if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
250
- logMissingArg(arg);
266
+ const sizeLimit = Number(argv[index + 1]);
251
267
 
252
- process.exit(1);
253
- }
268
+ if (isNaN(sizeLimit)) {
269
+ logInvalidNumber(arg, sizeLimit);
254
270
 
255
- rawArgs.videoPath = args[index + 1];
271
+ process.exit(1);
272
+ }
256
273
 
257
- skipNext();
274
+ rawArgs.sizeLimit = sizeLimit;
258
275
 
259
- continue;
260
- }
276
+ skipNext();
261
277
 
262
- if (arg === "-crf") {
263
- if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
264
- logMissingArg(arg);
278
+ continue;
279
+ }
265
280
 
266
- process.exit(1);
267
- }
281
+ if (arg === "-ep" || arg === "--extra-params") {
282
+ if (argv[index + 1] === undefined) {
283
+ logMissingArg(arg);
268
284
 
269
- const crf = Number(args[index + 1]);
285
+ process.exit(1);
286
+ }
270
287
 
271
- if (isNaN(crf)) {
272
- logInvalidNumber(arg, crf);
288
+ isExtraParam = true;
273
289
 
274
- process.exit(1);
275
- }
290
+ rawArgs.extraParams = [];
276
291
 
277
- rawArgs.crf = crf;
292
+ continue;
293
+ }
278
294
 
279
- skipNext();
295
+ if (arg === "--video-path") {
296
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith("-")) {
297
+ logMissingArg(arg);
280
298
 
281
- continue;
299
+ process.exit(1);
282
300
  }
283
301
 
284
- if (arg === "-cpu-used") {
285
- if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
286
- logMissingArg(arg);
302
+ rawArgs.videoPath = argv[index + 1];
287
303
 
288
- process.exit(1);
289
- }
304
+ skipNext();
290
305
 
291
- const cpuUsed = Number(args[index + 1]);
306
+ continue;
307
+ }
292
308
 
293
- if (isNaN(cpuUsed)) {
294
- logInvalidNumber(arg, cpuUsed);
309
+ if (arg === "--no-log-file") {
310
+ rawArgs.noLogFile = true;
295
311
 
296
- process.exit(1);
297
- }
312
+ continue;
313
+ }
298
314
 
299
- if (ArgsSchema.shape.cpuUsed.safeParse(cpuUsed).success === false) {
300
- logger.error(
301
- `The ${arg} flag requires a number between 0 and 5 inclusive. "${cpuUsed}" is out of that range`,
302
- );
315
+ if (arg === "-crf") {
316
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith("-")) {
317
+ logMissingArg(arg);
303
318
 
304
- process.exit(1);
305
- }
319
+ process.exit(1);
320
+ }
306
321
 
307
- rawArgs.cpuUsed = cpuUsed as 0 | 1 | 2 | 3 | 4 | 5;
322
+ const crf = Number(argv[index + 1]);
308
323
 
309
- skipNext();
324
+ if (isNaN(crf)) {
325
+ logInvalidNumber(arg, crf);
310
326
 
311
- continue;
327
+ process.exit(1);
312
328
  }
313
329
 
314
- if (arg === "-deadline") {
315
- if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
316
- logMissingArg(arg);
330
+ rawArgs.crf = crf;
317
331
 
318
- process.exit(1);
319
- }
332
+ skipNext();
320
333
 
321
- if (!["good", "best"].includes(args[index + 1])) {
322
- logger.error(
323
- `The ${arg} flag requires either "good" or "best". "${args[index + 1]}" is not a valid value`,
324
- );
334
+ continue;
335
+ }
325
336
 
326
- process.exit(1);
327
- }
337
+ if (arg === "-cpu-used") {
338
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith("-")) {
339
+ logMissingArg(arg);
340
+
341
+ process.exit(1);
342
+ }
343
+
344
+ const cpuUsed = Number(argv[index + 1]);
345
+
346
+ if (isNaN(cpuUsed)) {
347
+ logInvalidNumber(arg, cpuUsed);
328
348
 
329
- rawArgs.deadline = args[index + 1] as "good" | "best";
349
+ process.exit(1);
350
+ }
330
351
 
331
- skipNext();
352
+ if (ArgsSchema.shape.cpuUsed.safeParse(cpuUsed).success === false) {
353
+ console.error(
354
+ `The ${arg} flag requires a number between 0 and 5 inclusive. "${cpuUsed}" is out of that range`,
355
+ );
332
356
 
333
- continue;
357
+ process.exit(1);
334
358
  }
335
359
 
336
- if (arg === "-c:v") {
337
- if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
338
- logMissingArg(arg);
360
+ rawArgs.cpuUsed = cpuUsed as 0 | 1 | 2 | 3 | 4 | 5;
339
361
 
340
- process.exit(1);
341
- }
362
+ skipNext();
342
363
 
343
- rawArgs.encoder = args[index + 1];
364
+ continue;
365
+ }
344
366
 
345
- skipNext();
367
+ if (arg === "-deadline") {
368
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith("-")) {
369
+ logMissingArg(arg);
346
370
 
347
- continue;
371
+ process.exit(1);
348
372
  }
349
373
 
350
- if (arg === "-lavfi") {
351
- if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
352
- logMissingArg(arg);
374
+ if (!["good", "best"].includes(argv[index + 1])) {
375
+ console.error(
376
+ `The ${arg} flag requires either "good" or "best". "${argv[index + 1]}" is not a valid value`,
377
+ );
353
378
 
354
- process.exit(1);
355
- }
379
+ process.exit(1);
380
+ }
356
381
 
357
- rawArgs.lavfi = args[index + 1];
382
+ rawArgs.deadline = argv[index + 1] as "good" | "best";
358
383
 
359
- skipNext();
384
+ skipNext();
360
385
 
361
- continue;
362
- }
386
+ continue;
363
387
  }
364
388
 
365
- if (seeking) {
366
- rawArgs.output = { ...rawArgs.output, ...seeking };
389
+ if (arg === "-c:v") {
390
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith("-")) {
391
+ logMissingArg(arg);
367
392
 
368
- seeking = undefined;
369
- }
393
+ process.exit(1);
394
+ }
370
395
 
371
- if (!rawArgs.inputs) {
372
- printUsage();
396
+ rawArgs.encoder = argv[index + 1];
373
397
 
374
- // if the user isn't using any of the quick actions we require the -i flag
375
- logger.error("Input file is required");
398
+ skipNext();
376
399
 
377
- process.exit(1);
400
+ continue;
378
401
  }
379
402
 
380
- const parsedArgs = ArgsSchema.safeParse(rawArgs);
403
+ if (arg === "-lavfi") {
404
+ if (argv[index + 1] === undefined || argv[index + 1].startsWith("-")) {
405
+ logMissingArg(arg);
406
+
407
+ process.exit(1);
408
+ }
381
409
 
382
- if (!parsedArgs.success) {
383
- logger.error("Error parsing the arguments");
410
+ rawArgs.lavfi = argv[index + 1];
384
411
 
385
- logger.error(
386
- JSON.stringify(parsedArgs.error.flatten().fieldErrors, null, 2),
387
- );
412
+ skipNext();
388
413
 
389
- process.exit(1);
414
+ continue;
390
415
  }
416
+ }
391
417
 
392
- return parsedArgs.data;
393
- };
418
+ if (seeking) {
419
+ rawArgs.output = { ...rawArgs.output, ...seeking };
394
420
 
395
- const logMissingArg = (arg: string) =>
396
- logger.error(`The ${arg} flag requires an argument`);
421
+ seeking = undefined;
422
+ }
397
423
 
398
- const logInvalidNumber = (arg: string, value: number) =>
399
- logger.error(
400
- `The ${arg} flag requires a number. "${value}" is not a valid number`,
401
- );
424
+ if (!rawArgs.inputs) {
425
+ printUsage();
402
426
 
403
- const printUsage = () => {
404
- const usage = `Usage: ${CLI_NAME} [options] [[infile options] -i infile]... [outfile options] [outfile] [extra params]
427
+ // if the user isn't using any of the quick actions we require the -i flag
428
+ console.error("Input file is required");
405
429
 
406
- ${DESCRIPTION}
430
+ process.exit(1);
431
+ }
407
432
 
408
- Positional arguments:
409
- [outfile] The output file. If not specified, a generated unix timestamp as filename will be used
410
- and saved to the directory set in the --video-path option
433
+ const parsed = ArgsSchema.safeParse(rawArgs);
411
434
 
412
- Options:
413
- -h, --help Show this help message
414
- -v, --version Show version information
415
- -kill Terminate the running ${CLI_NAME} instance, if there is any
416
- -status Show the current status
417
- -i <input> The input file to encode
418
- -ss <start_time> The start time (same as ffmpeg's -ss)
419
- -to <stop_time> The stop time (same as ffmpeg's -to)
420
- -lavfi <filters> The set of filters to pass to ffmpeg
421
- -c:v <encoder> The video encoder to use (default is ${config.encoder})
422
- -deadline {good,best} The deadline for libvpx-vp9; good is the recommended one, best has the best
423
- compression efficiency but takes the most time (default is ${config.deadline})
424
- -crf <value> The crf to use (default is ${config.crf})
425
- -cpu-used {0,1,2,3,4,5} The cpu-used for libvpx-vp9; a number between 0 and 5 inclusive, the higher
426
- the number the faster the encoding will be with a quality trade-off (default is ${config.cpuUsed})
427
- -subs Burn the subtitles onto the output file; this flag will automatically use
428
- the subtitles found in the first input file, to use a different file use
429
- the -lavfi flag with the subtitles filter directly
430
- -sl, --size-limit <limit> The size limit of the output file in MiB, use 0 for no limit (default is ${config.sizeLimit})
431
- --video-path <path> The video path where the video files are stored (default is ${config.videoPath})
432
- this is overridden if the output file is specified
433
- -ep, --extra-params <params>
434
- The extra parameters to pass to ffmpeg, these will be appended making it
435
- possible to override some defaults. This option has to be the last one, everything
436
- will be passed as is to ffmpeg
435
+ if (!parsed.success) {
436
+ console.error("Error parsing the arguments");
437
437
 
438
- Examples:
439
- ${CLI_NAME} -i "/tmp/Videos/nijinosaki.mkv" -ss 00:00:02.268 -to 00:00:10.310
440
- ${CLI_NAME} -i "/tmp/Videos/nijinosaki.mkv" --size-limit 6 -subs --extra-params -map 0:a -c:a libopus -b:a 128k`;
438
+ console.error(JSON.stringify(parsed.error.flatten().fieldErrors, null, 2));
441
439
 
442
- logger.info(usage, {
443
- onlyConsole: true,
444
- });
445
- };
440
+ process.exit(1);
441
+ }
442
+
443
+ export const parsedArgs = parsed.data;
package/src/config.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  import path from "path";
2
2
 
3
3
  import { parse } from "smol-toml";
4
- import { logger } from "./logger";
5
4
  import { CONFIG_PATH } from "./paths";
6
5
  import { ConfigSchema } from "./schema/config";
7
6
  import { CONFIG_FILE_NAME } from "./constants";
@@ -12,13 +11,11 @@ const configPath = path.join(CONFIG_PATH, CONFIG_FILE_NAME);
12
11
  let rawConfig: unknown = {};
13
12
 
14
13
  if (existsSync(configPath)) {
15
- logger.info("Loading config file " + configPath);
16
-
17
14
  try {
18
15
  rawConfig = parse(readFileSync(configPath, "utf-8"));
19
16
  } catch (error) {
20
17
  if (error instanceof Error) {
21
- logger.error(
18
+ console.error(
22
19
  "Error parsing the config file " + configPath + ":\n\n" + error.message,
23
20
  );
24
21
 
@@ -32,12 +29,12 @@ if (existsSync(configPath)) {
32
29
  const parsedConfig = ConfigSchema.safeParse(rawConfig);
33
30
 
34
31
  if (!parsedConfig.success) {
35
- logger.error("Error parsing the config file " + configPath);
32
+ console.error("Error parsing the config file " + configPath);
36
33
 
37
34
  const errors = parsedConfig.error.flatten().fieldErrors;
38
35
 
39
36
  for (const key in errors) {
40
- logger.error(
37
+ console.error(
41
38
  `Error in option "${key}": ` +
42
39
  errors[key as keyof typeof errors]?.join("; "),
43
40
  );
@@ -0,0 +1,66 @@
1
+ import { SOCKET_FILE } from "./constants";
2
+ import { ResponseSchema } from "../schema/ipc";
3
+
4
+ import type { IPCSchema } from "../schema/ipc";
5
+ import type { StatusSchema } from "../schema/status";
6
+
7
+ // overloading
8
+ type SendMessage = {
9
+ (message: Exclude<IPCSchema, { type: "status" }>): Promise<void>;
10
+ (message: Extract<IPCSchema, { type: "status" }>): Promise<StatusSchema>;
11
+ };
12
+
13
+ const sendMessage: SendMessage = async (message) =>
14
+ new Promise(async (resolve, reject) => {
15
+ try {
16
+ const socket = await Bun.connect({
17
+ unix: SOCKET_FILE,
18
+ socket: {
19
+ data: (socket, rawData) => {
20
+ socket.end();
21
+
22
+ const parsedData = ResponseSchema.safeParse(
23
+ JSON.parse(rawData.toString()),
24
+ );
25
+
26
+ if (!parsedData.success) {
27
+ // couldn't parse
28
+ console.error("Invalid response received through the socket");
29
+
30
+ return reject();
31
+ }
32
+
33
+ if (!parsedData.data.success) {
34
+ // request did not succeed
35
+ return reject();
36
+ }
37
+
38
+ if (parsedData.data.type === "status") {
39
+ const { data } = parsedData.data;
40
+
41
+ return resolve(data);
42
+ }
43
+
44
+ return resolve(undefined as void & StatusSchema);
45
+ },
46
+ },
47
+ });
48
+
49
+ socket.write(JSON.stringify(message));
50
+ } catch (error) {
51
+ if (
52
+ error instanceof Error &&
53
+ "code" in error &&
54
+ error.code === "ENOENT" &&
55
+ message.type !== "enqueue"
56
+ ) {
57
+ console.error("No current main instance running");
58
+ }
59
+
60
+ reject(error);
61
+ }
62
+ });
63
+
64
+ export const ipcClient = {
65
+ sendMessage,
66
+ };
@@ -0,0 +1,9 @@
1
+ import os from "os";
2
+ import path from "path";
3
+
4
+ import { TEMP_PATH } from "../paths";
5
+ import { PIPE_NAME, SOCKET_NAME } from "../constants";
6
+
7
+ // windows doesn't have unix sockets but named pipes
8
+ export const SOCKET_FILE =
9
+ os.platform() === "win32" ? PIPE_NAME : path.join(TEMP_PATH, SOCKET_NAME);
@@ -1,21 +1,14 @@
1
- import os from "os";
2
- import path from "path";
3
-
4
- import { queue } from "./queue";
5
- import { status } from "./status";
6
- import { logger } from "./logger";
7
- import { TEMP_PATH } from "./paths";
1
+ import { queue } from "../queue";
2
+ import { status } from "../status";
3
+ import { logger } from "../logger";
8
4
  import { unlinkSync } from "fs";
9
- import { assertNever } from "./utils";
10
- import { PIPE_NAME, SOCKET_NAME } from "./constants";
11
- import { IPCSchema, ResponseSchema } from "./schema/ipc";
5
+ import { assertNever } from "../utils";
6
+ import { SOCKET_FILE } from "./constants";
7
+ import { IPCSchema, ResponseSchema } from "../schema/ipc";
12
8
 
13
- import type { StatusSchema } from "./schema/status";
14
- import type { UnixSocketListener } from "bun";
9
+ import os from "os";
15
10
 
16
- // windows doesn't have unix sockets but named pipes
17
- const SOCKET_FILE =
18
- os.platform() === "win32" ? PIPE_NAME : path.join(TEMP_PATH, SOCKET_NAME);
11
+ import type { UnixSocketListener } from "bun";
19
12
 
20
13
  let listener: UnixSocketListener<undefined> | undefined;
21
14
 
@@ -72,8 +65,6 @@ const startListener = () => {
72
65
  );
73
66
  break;
74
67
  case "status":
75
- logger.info("Received status request, sending the current status");
76
-
77
68
  socket.end(
78
69
  JSON.stringify({
79
70
  type: "status",
@@ -116,67 +107,7 @@ const stopListener = () => {
116
107
  }
117
108
  };
118
109
 
119
- // overloading
120
- type SendMessage = {
121
- (message: Exclude<IPCSchema, { type: "status" }>): Promise<void>;
122
- (message: Extract<IPCSchema, { type: "status" }>): Promise<StatusSchema>;
123
- };
124
-
125
- const sendMessage: SendMessage = async (message) =>
126
- new Promise(async (resolve, reject) => {
127
- try {
128
- const socket = await Bun.connect({
129
- unix: SOCKET_FILE,
130
- socket: {
131
- data: (socket, rawData) => {
132
- socket.end();
133
-
134
- const parsedData = ResponseSchema.safeParse(
135
- JSON.parse(rawData.toString()),
136
- );
137
-
138
- if (!parsedData.success) {
139
- // couldn't parse
140
- logger.error("Invalid response received through the socket");
141
-
142
- return reject();
143
- }
144
-
145
- if (!parsedData.data.success) {
146
- // request did not succeed
147
- return reject();
148
- }
149
-
150
- if (parsedData.data.type === "status") {
151
- const { data } = parsedData.data;
152
-
153
- return resolve(data);
154
- }
155
-
156
- return resolve(undefined as void & StatusSchema);
157
- },
158
- },
159
- });
160
-
161
- socket.write(JSON.stringify(message));
162
- } catch (error) {
163
- if (
164
- error instanceof Error &&
165
- "code" in error &&
166
- error.code === "ENOENT" &&
167
- message.type !== "enqueue"
168
- ) {
169
- logger.info("No current main instance running", {
170
- logToConsole: true,
171
- });
172
- }
173
-
174
- reject(error);
175
- }
176
- });
177
-
178
- export const ipc = {
179
- sendMessage,
180
- stopListener,
181
- startListener,
110
+ export const ipcServer = {
111
+ stop: stopListener,
112
+ start: startListener,
182
113
  };
package/src/logger.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  import os from "os";
2
2
  import path from "path";
3
3
 
4
+ import { parsedArgs } from "./args";
4
5
  import { CONFIG_PATH } from "./paths";
5
6
  import { CLI_NAME, LOG_FILE_NAME } from "./constants";
6
7
  import { existsSync, mkdirSync, appendFileSync } from "fs";
@@ -113,6 +114,10 @@ const log = (message: Message, level: Level, options?: Options) => {
113
114
  consoleLog(consoleMessage);
114
115
  }
115
116
 
117
+ if (parsedArgs.noLogFile) {
118
+ return;
119
+ }
120
+
116
121
  writeToFile(message, level);
117
122
  };
118
123
 
package/src/main.ts CHANGED
@@ -1,9 +1,14 @@
1
- import { ipc } from "./ipc";
2
1
  import { queue } from "./queue";
3
2
  import { logger } from "./logger";
4
3
  import { CLI_NAME } from "./constants";
5
- import { parseArgs } from "./args";
6
4
  import { cleanExit } from "./utils";
5
+ import { ipcServer } from "./ipc/server";
6
+ import { ipcClient } from "./ipc/client";
7
+ import { parsedArgs } from "./args";
8
+
9
+ const argv = Bun.argv.slice(2);
10
+
11
+ logger.info("argv: " + argv.join(" "));
7
12
 
8
13
  process.title = CLI_NAME;
9
14
 
@@ -51,12 +56,6 @@ process.on("unhandledRejection", (reason) => {
51
56
  cleanExit(1);
52
57
  });
53
58
 
54
- const args = Bun.argv.slice(2);
55
-
56
- logger.info("argv: " + args.join(" "));
57
-
58
- const parsedArgs = await parseArgs(args);
59
-
60
59
  // let's check if ffmpeg and ffprobe are available before encoding anything
61
60
  try {
62
61
  const ffmpegProcess = Bun.spawnSync(["ffmpeg", "-hide_banner", "-version"]);
@@ -90,7 +89,7 @@ try {
90
89
 
91
90
  try {
92
91
  // try sending the args to the running process if there is one
93
- await ipc.sendMessage({
92
+ await ipcClient.sendMessage({
94
93
  type: "enqueue",
95
94
  data: parsedArgs,
96
95
  });
@@ -103,9 +102,9 @@ try {
103
102
 
104
103
  queue.push(parsedArgs);
105
104
 
106
- ipc.startListener();
105
+ ipcServer.start();
107
106
 
108
107
  await queue.processQueue();
109
108
 
110
- ipc.stopListener();
109
+ ipcServer.stop();
111
110
  }
@@ -28,6 +28,7 @@ export const ArgsSchema = ConfigSchema.merge(
28
28
  encoder: ConfigSchema.shape.encoder.default(config.encoder),
29
29
  cpuUsed: ConfigSchema.shape.cpuUsed.default(config.cpuUsed),
30
30
  deadline: ConfigSchema.shape.deadline.default(config.deadline),
31
+ noLogFile: ConfigSchema.shape.noLogFile.default(config.noLogFile),
31
32
  sizeLimit: ConfigSchema.shape.sizeLimit.default(config.sizeLimit),
32
33
  videoPath: ConfigSchema.shape.videoPath.default(config.videoPath),
33
34
  }),
@@ -3,6 +3,7 @@ import { DEFAULT_VIDEO_PATH, expandHome } from "../paths";
3
3
 
4
4
  export const ConfigSchema = z.object({
5
5
  subs: z.boolean().default(false),
6
+ noLogFile: z.boolean().default(false),
6
7
  encoder: z.string().default("libvpx-vp9"),
7
8
  sizeLimit: z.number().default(4),
8
9
  crf: z.number().default(24),
package/src/utils.ts CHANGED
@@ -1,8 +1,10 @@
1
- import { ipc } from "./ipc";
2
1
  import { queue } from "./queue";
3
2
  import { logger } from "./logger";
3
+ import { ipcServer } from "./ipc/server";
4
4
 
5
- export const assertNever = (value: never) => {
5
+ type AssertNever = (value: never) => never;
6
+
7
+ export const assertNever: AssertNever = (value) => {
6
8
  throw new Error(`Unexpected value: ${value}`);
7
9
  };
8
10
 
@@ -15,7 +17,7 @@ export const cleanExit: CleanExit = async (code = 0) => {
15
17
 
16
18
  await queue.abortProcessing();
17
19
 
18
- ipc.stopListener();
20
+ ipcServer.stop();
19
21
 
20
22
  process.exit(code);
21
23
  };