fluncle 0.78.0 → 0.80.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/bin/fluncle.mjs +408 -43
- package/package.json +1 -1
package/bin/fluncle.mjs
CHANGED
|
@@ -558,7 +558,7 @@ function parseVersion(version) {
|
|
|
558
558
|
var currentVersion;
|
|
559
559
|
var init_version = __esm(() => {
|
|
560
560
|
init_output();
|
|
561
|
-
currentVersion = "0.
|
|
561
|
+
currentVersion = "0.80.0".trim() ? "0.80.0".trim() : "0.1.0";
|
|
562
562
|
});
|
|
563
563
|
|
|
564
564
|
// src/update-notifier.ts
|
|
@@ -982,6 +982,7 @@ function mapTrack(track) {
|
|
|
982
982
|
videoGrain: track.videoGrain,
|
|
983
983
|
videoModel: track.videoModel,
|
|
984
984
|
videoModelReasoning: track.videoModelReasoning,
|
|
985
|
+
videoRegister: track.videoRegister,
|
|
985
986
|
videoUrl: track.videoUrl,
|
|
986
987
|
videoVehicle: track.videoVehicle
|
|
987
988
|
};
|
|
@@ -1351,6 +1352,7 @@ var init_mixtape_mixcloud = __esm(() => {
|
|
|
1351
1352
|
// src/commands/mixtape-set-video.ts
|
|
1352
1353
|
var exports_mixtape_set_video = {};
|
|
1353
1354
|
__export(exports_mixtape_set_video, {
|
|
1355
|
+
uploadRenditionMultipart: () => uploadRenditionMultipart,
|
|
1354
1356
|
stageSetVideo: () => stageSetVideo,
|
|
1355
1357
|
renditionFfmpegArgs: () => renditionFfmpegArgs,
|
|
1356
1358
|
planMultipart: () => planMultipart,
|
|
@@ -1417,7 +1419,7 @@ function renditionFfmpegArgs(inputPath, outputPath) {
|
|
|
1417
1419
|
outputPath
|
|
1418
1420
|
];
|
|
1419
1421
|
}
|
|
1420
|
-
async function
|
|
1422
|
+
async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
|
|
1421
1423
|
if (!existsSync(masterPath)) {
|
|
1422
1424
|
throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
|
|
1423
1425
|
}
|
|
@@ -1429,7 +1431,9 @@ async function stageSetVideo(mixtapeId, masterPath, onProgress = () => {}) {
|
|
|
1429
1431
|
const size = statSync2(renditionPath).size;
|
|
1430
1432
|
const plan = planMultipart(size);
|
|
1431
1433
|
onProgress(`Set video: uploading ${(size / 1e9).toFixed(2)} GB in ${plan.partCount} part(s)…`);
|
|
1432
|
-
const presign = await adminApiPost(
|
|
1434
|
+
const presign = await adminApiPost(presignPath, {
|
|
1435
|
+
partCount: plan.partCount
|
|
1436
|
+
});
|
|
1433
1437
|
const urlByPart = new Map(presign.parts.map((part) => [part.partNumber, part.url]));
|
|
1434
1438
|
const completed = [];
|
|
1435
1439
|
try {
|
|
@@ -1447,13 +1451,19 @@ async function stageSetVideo(mixtapeId, masterPath, onProgress = () => {}) {
|
|
|
1447
1451
|
await abortUpload(presign.abortUrl).catch(() => {});
|
|
1448
1452
|
throw error;
|
|
1449
1453
|
}
|
|
1450
|
-
await adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}`, { setVideoAt: new Date().toISOString() });
|
|
1451
|
-
onProgress("Set video: flipped setVideoAt — the /log player + video SEO are live.");
|
|
1452
1454
|
return { key: presign.key, url: `${FOUND_BASE}/${presign.key}` };
|
|
1453
1455
|
} finally {
|
|
1454
1456
|
rmSync2(renditionPath, { force: true });
|
|
1455
1457
|
}
|
|
1456
1458
|
}
|
|
1459
|
+
async function stageSetVideo(mixtapeId, masterPath, onProgress = () => {}) {
|
|
1460
|
+
const result = await uploadRenditionMultipart(masterPath, `/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/set-video/presign`, onProgress);
|
|
1461
|
+
await adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}`, {
|
|
1462
|
+
setVideoAt: new Date().toISOString()
|
|
1463
|
+
});
|
|
1464
|
+
onProgress("Set video: flipped setVideoAt — the /log player + video SEO are live.");
|
|
1465
|
+
return result;
|
|
1466
|
+
}
|
|
1457
1467
|
async function putPart(url, path2, part) {
|
|
1458
1468
|
const response = await fetch(url, {
|
|
1459
1469
|
body: Bun.file(path2).slice(part.start, part.end),
|
|
@@ -1943,6 +1953,7 @@ function mapTrack2(track) {
|
|
|
1943
1953
|
videoGrain: track.videoGrain,
|
|
1944
1954
|
videoModel: track.videoModel,
|
|
1945
1955
|
videoModelReasoning: track.videoModelReasoning,
|
|
1956
|
+
videoRegister: track.videoRegister,
|
|
1946
1957
|
videoUrl: track.videoUrl,
|
|
1947
1958
|
videoVehicle: track.videoVehicle
|
|
1948
1959
|
};
|
|
@@ -2455,7 +2466,7 @@ function parseVersion2(version) {
|
|
|
2455
2466
|
var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
|
|
2456
2467
|
var init_version2 = __esm(() => {
|
|
2457
2468
|
init_output();
|
|
2458
|
-
currentVersion2 = "0.
|
|
2469
|
+
currentVersion2 = "0.80.0".trim() ? "0.80.0".trim() : "0.1.0";
|
|
2459
2470
|
});
|
|
2460
2471
|
|
|
2461
2472
|
// ../../packages/registry/src/index.ts
|
|
@@ -3220,7 +3231,8 @@ async function trackVideoCommand(idOrLogId, files, onProgress) {
|
|
|
3220
3231
|
videoModelReasoning,
|
|
3221
3232
|
...squared ? { squared: true } : {},
|
|
3222
3233
|
...manifest.vehicle ? { videoVehicle: manifest.vehicle } : {},
|
|
3223
|
-
...manifest.grain ? { videoGrain: manifest.grain } : {}
|
|
3234
|
+
...manifest.grain ? { videoGrain: manifest.grain } : {},
|
|
3235
|
+
...manifest.register ? { videoRegister: manifest.register } : {}
|
|
3224
3236
|
});
|
|
3225
3237
|
return { logId: finalize.logId, ok: true, trackId: finalize.trackId, urls };
|
|
3226
3238
|
}
|
|
@@ -3228,7 +3240,7 @@ async function readManifestFields(renderPath) {
|
|
|
3228
3240
|
try {
|
|
3229
3241
|
const manifest = await Bun.file(renderPath).json();
|
|
3230
3242
|
const result = {};
|
|
3231
|
-
for (const key of ["vehicle", "grain", "model", "reasoning"]) {
|
|
3243
|
+
for (const key of ["vehicle", "grain", "model", "reasoning", "register"]) {
|
|
3232
3244
|
const value = manifest[key];
|
|
3233
3245
|
if (typeof value === "string" && value.trim()) {
|
|
3234
3246
|
result[key] = value.trim().slice(0, 120);
|
|
@@ -3467,6 +3479,7 @@ async function vehiclesCommand(limit) {
|
|
|
3467
3479
|
artists: track.artists,
|
|
3468
3480
|
grain: track.videoGrain,
|
|
3469
3481
|
logId: track.logId,
|
|
3482
|
+
register: track.videoRegister,
|
|
3470
3483
|
title: track.title,
|
|
3471
3484
|
vehicle: track.videoVehicle
|
|
3472
3485
|
}));
|
|
@@ -3842,6 +3855,145 @@ var init_mixtape_youtube2 = __esm(() => {
|
|
|
3842
3855
|
init_output();
|
|
3843
3856
|
});
|
|
3844
3857
|
|
|
3858
|
+
// src/commands/recordings.ts
|
|
3859
|
+
var exports_recordings = {};
|
|
3860
|
+
__export(exports_recordings, {
|
|
3861
|
+
recordingsListCommand: () => recordingsListCommand,
|
|
3862
|
+
recordingUpdateCommand: () => recordingUpdateCommand,
|
|
3863
|
+
recordingPromoteCommand: () => recordingPromoteCommand,
|
|
3864
|
+
recordingGetCommand: () => recordingGetCommand,
|
|
3865
|
+
recordingGet: () => recordingGet,
|
|
3866
|
+
recordingDeleteCommand: () => recordingDeleteCommand,
|
|
3867
|
+
recordingCreateCommand: () => recordingCreateCommand
|
|
3868
|
+
});
|
|
3869
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
3870
|
+
async function recordingGet(id) {
|
|
3871
|
+
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
3872
|
+
return response.recording;
|
|
3873
|
+
}
|
|
3874
|
+
function formatRecordingSummary(recording) {
|
|
3875
|
+
const promoted = recording.logId ? ` → fluncle://${recording.logId}` : " (un-promoted)";
|
|
3876
|
+
const cues = recording.tracklist.length;
|
|
3877
|
+
return `${recording.id} ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
|
|
3878
|
+
}
|
|
3879
|
+
async function recordingsListCommand(options = {}) {
|
|
3880
|
+
const response = await adminApiGet("/api/admin/recordings");
|
|
3881
|
+
if (options.json) {
|
|
3882
|
+
printJson2({ ok: true, recordings: response.recordings });
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
if (response.recordings.length === 0) {
|
|
3886
|
+
console.log("No recordings yet.");
|
|
3887
|
+
return;
|
|
3888
|
+
}
|
|
3889
|
+
console.log(response.recordings.map(formatRecordingSummary).join(`
|
|
3890
|
+
`));
|
|
3891
|
+
}
|
|
3892
|
+
async function recordingGetCommand(id, options = {}) {
|
|
3893
|
+
if (!id) {
|
|
3894
|
+
throw new CliError2("missing_id", "Missing recording id for: get");
|
|
3895
|
+
}
|
|
3896
|
+
const recording = await recordingGet(id);
|
|
3897
|
+
if (options.json) {
|
|
3898
|
+
printJson2({ ok: true, recording });
|
|
3899
|
+
return;
|
|
3900
|
+
}
|
|
3901
|
+
console.log(formatRecordingSummary(recording));
|
|
3902
|
+
console.log(` r2Key: ${recording.r2Key}`);
|
|
3903
|
+
for (const cue of recording.tracklist) {
|
|
3904
|
+
const at = cue.startMs === undefined ? "—" : `${(cue.startMs / 1000).toFixed(1)}s`;
|
|
3905
|
+
console.log(` ${at} ${cue.artists.join(", ")} — ${cue.title}`);
|
|
3906
|
+
}
|
|
3907
|
+
}
|
|
3908
|
+
async function recordingCreateCommand(options = {}) {
|
|
3909
|
+
const title = options.title?.trim();
|
|
3910
|
+
if (!title) {
|
|
3911
|
+
throw new CliError2("missing_title", "A recording needs a --title");
|
|
3912
|
+
}
|
|
3913
|
+
if (!options.video) {
|
|
3914
|
+
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
3915
|
+
}
|
|
3916
|
+
if (!existsSync3(options.video)) {
|
|
3917
|
+
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
3918
|
+
}
|
|
3919
|
+
const created = await adminApiPost("/api/admin/recordings", {
|
|
3920
|
+
recordedAt: options.recordedAt,
|
|
3921
|
+
title
|
|
3922
|
+
});
|
|
3923
|
+
const recording = created.recording;
|
|
3924
|
+
const log = (message) => {
|
|
3925
|
+
if (!options.json) {
|
|
3926
|
+
console.log(message);
|
|
3927
|
+
}
|
|
3928
|
+
};
|
|
3929
|
+
log(`Recording ${recording.id} created — staging the set video…`);
|
|
3930
|
+
const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
|
|
3931
|
+
if (options.json) {
|
|
3932
|
+
printJson2({ key: upload.key, ok: true, recording, url: upload.url });
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
3935
|
+
console.log(`Recording ${recording.id} staged → ${upload.url}`);
|
|
3936
|
+
}
|
|
3937
|
+
async function recordingUpdateCommand(id, options = {}) {
|
|
3938
|
+
if (!id) {
|
|
3939
|
+
throw new CliError2("missing_id", "Missing recording id for: update");
|
|
3940
|
+
}
|
|
3941
|
+
const body = {};
|
|
3942
|
+
if (options.title !== undefined) {
|
|
3943
|
+
body.title = options.title;
|
|
3944
|
+
}
|
|
3945
|
+
if (options.recordedAt !== undefined) {
|
|
3946
|
+
body.recordedAt = options.recordedAt;
|
|
3947
|
+
}
|
|
3948
|
+
if (options.tracklistFile !== undefined) {
|
|
3949
|
+
if (!existsSync3(options.tracklistFile)) {
|
|
3950
|
+
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
3951
|
+
}
|
|
3952
|
+
try {
|
|
3953
|
+
body.tracklistJson = JSON.parse(readFileSync3(options.tracklistFile, "utf8"));
|
|
3954
|
+
} catch {
|
|
3955
|
+
throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
if (Object.keys(body).length === 0) {
|
|
3959
|
+
throw new CliError2("no_fields", "Nothing to update — pass --title, --recorded-at, or --tracklist-file");
|
|
3960
|
+
}
|
|
3961
|
+
const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
|
|
3962
|
+
if (options.json) {
|
|
3963
|
+
printJson2({ ok: true, recording: response.recording });
|
|
3964
|
+
return;
|
|
3965
|
+
}
|
|
3966
|
+
console.log(`Updated ${formatRecordingSummary(response.recording)}`);
|
|
3967
|
+
}
|
|
3968
|
+
async function recordingDeleteCommand(id, options = {}) {
|
|
3969
|
+
if (!id) {
|
|
3970
|
+
throw new CliError2("missing_id", "Missing recording id for: delete");
|
|
3971
|
+
}
|
|
3972
|
+
await adminApiDelete(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
3973
|
+
if (options.json) {
|
|
3974
|
+
printJson2({ ok: true });
|
|
3975
|
+
return;
|
|
3976
|
+
}
|
|
3977
|
+
console.log(`Deleted recording ${id} (and its clips).`);
|
|
3978
|
+
}
|
|
3979
|
+
async function recordingPromoteCommand(id, options = {}) {
|
|
3980
|
+
if (!id) {
|
|
3981
|
+
throw new CliError2("missing_id", "Missing recording id for: promote");
|
|
3982
|
+
}
|
|
3983
|
+
const response = await adminApiPost(`/api/admin/recordings/${encodeURIComponent(id)}/promote`);
|
|
3984
|
+
const recording = response.recording;
|
|
3985
|
+
if (options.json) {
|
|
3986
|
+
printJson2({ ok: true, recording });
|
|
3987
|
+
return;
|
|
3988
|
+
}
|
|
3989
|
+
console.log(recording.logId ? `Promoted recording ${id} → mixtape fluncle://${recording.logId}` : `Promoted recording ${id}`);
|
|
3990
|
+
}
|
|
3991
|
+
var init_recordings = __esm(() => {
|
|
3992
|
+
init_api();
|
|
3993
|
+
init_output();
|
|
3994
|
+
init_mixtape_set_video();
|
|
3995
|
+
});
|
|
3996
|
+
|
|
3845
3997
|
// src/commands/mixtapes.ts
|
|
3846
3998
|
var exports_mixtapes2 = {};
|
|
3847
3999
|
__export(exports_mixtapes2, {
|
|
@@ -3856,7 +4008,7 @@ __export(exports_mixtapes2, {
|
|
|
3856
4008
|
mixtapeDeleteCommand: () => mixtapeDeleteCommand2,
|
|
3857
4009
|
mixtapeCreateCommand: () => mixtapeCreateCommand2
|
|
3858
4010
|
});
|
|
3859
|
-
import { existsSync as
|
|
4011
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
3860
4012
|
async function mixtapesCommand2() {
|
|
3861
4013
|
const response = await publicApiGet("/api/mixtapes");
|
|
3862
4014
|
return response.mixtapes;
|
|
@@ -4024,10 +4176,10 @@ function buildBody2(options) {
|
|
|
4024
4176
|
return body;
|
|
4025
4177
|
}
|
|
4026
4178
|
function parseCueFile2(filePath) {
|
|
4027
|
-
if (!
|
|
4179
|
+
if (!existsSync4(filePath)) {
|
|
4028
4180
|
throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
|
|
4029
4181
|
}
|
|
4030
|
-
const text =
|
|
4182
|
+
const text = readFileSync4(filePath, "utf-8");
|
|
4031
4183
|
const trimmed = text.trim();
|
|
4032
4184
|
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
4033
4185
|
try {
|
|
@@ -4124,7 +4276,7 @@ __export(exports_clips, {
|
|
|
4124
4276
|
BOX_CLIP_FONT_FILE: () => BOX_CLIP_FONT_FILE
|
|
4125
4277
|
});
|
|
4126
4278
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4127
|
-
import { existsSync as
|
|
4279
|
+
import { existsSync as existsSync5, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
|
|
4128
4280
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4129
4281
|
import { join as join5 } from "node:path";
|
|
4130
4282
|
function clipFootageKey(clipId) {
|
|
@@ -4135,7 +4287,7 @@ function resolveFontFile(envVar, bakedPath) {
|
|
|
4135
4287
|
if (explicit) {
|
|
4136
4288
|
return explicit;
|
|
4137
4289
|
}
|
|
4138
|
-
return
|
|
4290
|
+
return existsSync5(bakedPath) ? bakedPath : undefined;
|
|
4139
4291
|
}
|
|
4140
4292
|
function resolveClipFontFile() {
|
|
4141
4293
|
return resolveFontFile("CLIP_FONT_FILE", BOX_CLIP_FONT_FILE);
|
|
@@ -4185,24 +4337,32 @@ function resolveTitleLines(options, y) {
|
|
|
4185
4337
|
function clipCutFilterComplex(options) {
|
|
4186
4338
|
const xOffset = Math.max(0, Math.round(options.xOffset));
|
|
4187
4339
|
const crop = `crop=ih*9/16:ih:${xOffset}:0,scale=${CLIP_WIDTH}:${CLIP_HEIGHT}`;
|
|
4340
|
+
const logId = options.logId?.trim();
|
|
4341
|
+
const hasCoordinate = Boolean(logId);
|
|
4188
4342
|
const coordinateBox = Math.round(CLIP_COORDINATE_SIZE * 1.18);
|
|
4189
|
-
const titleBottomOffset = CLIP_SAFE_BOTTOM + coordinateBox + CLIP_LINE_GAP;
|
|
4343
|
+
const titleBottomOffset = hasCoordinate ? CLIP_SAFE_BOTTOM + coordinateBox + CLIP_LINE_GAP : CLIP_SAFE_BOTTOM;
|
|
4190
4344
|
const titleY = `h-${titleBottomOffset}-th`;
|
|
4191
4345
|
const titleLines = resolveTitleLines(options, titleY);
|
|
4192
|
-
const coordinate2 = {
|
|
4346
|
+
const coordinate2 = hasCoordinate ? {
|
|
4193
4347
|
fontFile: options.oxaniumFontFile,
|
|
4194
4348
|
size: CLIP_COORDINATE_SIZE,
|
|
4195
|
-
text: `fluncle://${
|
|
4349
|
+
text: `fluncle://${logId}`,
|
|
4196
4350
|
x: CLIP_MARGIN_X,
|
|
4197
4351
|
y: `h-${CLIP_SAFE_BOTTOM}-th`
|
|
4198
|
-
};
|
|
4352
|
+
} : undefined;
|
|
4199
4353
|
const haloGlyphs = [
|
|
4200
4354
|
...titleLines.map((line) => brandDrawtext({ ...line, color: CLIP_HALO_COLOR })),
|
|
4201
|
-
brandDrawtext({ ...coordinate2, color: CLIP_HALO_COLOR })
|
|
4355
|
+
...coordinate2 ? [brandDrawtext({ ...coordinate2, color: CLIP_HALO_COLOR })] : []
|
|
4202
4356
|
].join(",");
|
|
4203
4357
|
const sharpGlyphs = [
|
|
4204
4358
|
...titleLines.map((line) => brandDrawtext({ ...line, borderw: CLIP_SHARP_BORDERW, color: CLIP_TITLE_COLOR })),
|
|
4205
|
-
|
|
4359
|
+
...coordinate2 ? [
|
|
4360
|
+
brandDrawtext({
|
|
4361
|
+
...coordinate2,
|
|
4362
|
+
borderw: CLIP_SHARP_BORDERW,
|
|
4363
|
+
color: CLIP_COORDINATE_COLOR
|
|
4364
|
+
})
|
|
4365
|
+
] : []
|
|
4206
4366
|
].join(",");
|
|
4207
4367
|
return [
|
|
4208
4368
|
`[0:v]${crop},setsar=1[base]`,
|
|
@@ -4256,6 +4416,9 @@ function clipCutFfmpegArgs(options) {
|
|
|
4256
4416
|
}
|
|
4257
4417
|
async function clipsListCommand(filter = {}) {
|
|
4258
4418
|
const params = new URLSearchParams;
|
|
4419
|
+
if (filter.recordingId) {
|
|
4420
|
+
params.set("recordingId", filter.recordingId);
|
|
4421
|
+
}
|
|
4259
4422
|
if (filter.mixtapeId) {
|
|
4260
4423
|
params.set("mixtapeId", filter.mixtapeId);
|
|
4261
4424
|
}
|
|
@@ -4266,11 +4429,27 @@ async function clipsListCommand(filter = {}) {
|
|
|
4266
4429
|
const response = await adminApiGet(`/api/admin/clips${query ? `?${query}` : ""}`);
|
|
4267
4430
|
return response.clips;
|
|
4268
4431
|
}
|
|
4269
|
-
async function
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4432
|
+
async function resolveClipSource(clip) {
|
|
4433
|
+
if (clip.recordingId) {
|
|
4434
|
+
const { recordingGet: recordingGet2 } = await Promise.resolve().then(() => (init_recordings(), exports_recordings));
|
|
4435
|
+
const recording = await recordingGet2(clip.recordingId);
|
|
4436
|
+
if (!recording.r2Key) {
|
|
4437
|
+
throw new CliError2("recording_not_staged", `Recording ${clip.recordingId} has no staged set video`);
|
|
4438
|
+
}
|
|
4439
|
+
return {
|
|
4440
|
+
logId: recording.logId,
|
|
4441
|
+
members: recording.tracklist.map((cue) => ({
|
|
4442
|
+
artists: cue.artists,
|
|
4443
|
+
startMs: cue.startMs,
|
|
4444
|
+
title: cue.title
|
|
4445
|
+
})),
|
|
4446
|
+
setDurationMs: recording.durationMs ?? 0,
|
|
4447
|
+
setUrl: `${FOUND_BASE3}/${recording.r2Key.split("/").map(encodeURIComponent).join("/")}`,
|
|
4448
|
+
title: recording.title
|
|
4449
|
+
};
|
|
4450
|
+
}
|
|
4451
|
+
if (!clip.mixtapeId) {
|
|
4452
|
+
throw new CliError2("clip_unlinked", `Clip ${clip.id} is linked to neither a recording nor a mixtape`);
|
|
4274
4453
|
}
|
|
4275
4454
|
const { mixtapeGetCommand: mixtapeGetCommand2 } = await Promise.resolve().then(() => (init_mixtapes2(), exports_mixtapes2));
|
|
4276
4455
|
const mixtape = await mixtapeGetCommand2(clip.mixtapeId);
|
|
@@ -4280,22 +4459,36 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4280
4459
|
if (!mixtape.setVideoAt) {
|
|
4281
4460
|
throw new CliError2("set_not_staged", `Mixtape ${mixtape.logId} has no staged set video — run \`distribute --set-video\` first`);
|
|
4282
4461
|
}
|
|
4462
|
+
return {
|
|
4463
|
+
logId: mixtape.logId,
|
|
4464
|
+
members: mixtape.members,
|
|
4465
|
+
setDurationMs: mixtape.durationMs ?? 0,
|
|
4466
|
+
setUrl: setVideoUrl(mixtape.logId),
|
|
4467
|
+
title: mixtape.title
|
|
4468
|
+
};
|
|
4469
|
+
}
|
|
4470
|
+
async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
4471
|
+
const clips = await clipsListCommand();
|
|
4472
|
+
const clip = clips.find((candidate) => candidate.id === clipId);
|
|
4473
|
+
if (!clip) {
|
|
4474
|
+
throw new CliError2("clip_not_found", `No clip with id ${clipId}`);
|
|
4475
|
+
}
|
|
4476
|
+
const source = await resolveClipSource(clip);
|
|
4283
4477
|
await assertFfmpeg2();
|
|
4284
|
-
const setUrl = setVideoUrl(mixtape.logId);
|
|
4285
4478
|
const outputPath = join5(tmpdir2(), `fluncle-clip-${randomUUID2()}.mp4`);
|
|
4286
4479
|
try {
|
|
4287
|
-
onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms] from ${
|
|
4480
|
+
onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms]${source.logId ? ` from ${source.logId}` : ""}…`);
|
|
4288
4481
|
await runClipCut({
|
|
4289
4482
|
inMs: clip.inMs,
|
|
4290
|
-
logId:
|
|
4291
|
-
members:
|
|
4483
|
+
logId: source.logId,
|
|
4484
|
+
members: source.members,
|
|
4292
4485
|
outMs: clip.outMs,
|
|
4293
4486
|
outputPath,
|
|
4294
4487
|
oxaniumFontFile: resolveClipFontFile(),
|
|
4295
4488
|
sansFontFile: resolveClipSansFontFile(),
|
|
4296
|
-
setDurationMs:
|
|
4297
|
-
setUrl,
|
|
4298
|
-
title:
|
|
4489
|
+
setDurationMs: source.setDurationMs,
|
|
4490
|
+
setUrl: source.setUrl,
|
|
4491
|
+
title: source.title,
|
|
4299
4492
|
xOffset: clip.xOffset
|
|
4300
4493
|
});
|
|
4301
4494
|
const sizeBytes = statSync4(outputPath).size;
|
|
@@ -4310,7 +4503,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4310
4503
|
return {
|
|
4311
4504
|
clipId,
|
|
4312
4505
|
key: presign.key,
|
|
4313
|
-
logId:
|
|
4506
|
+
logId: source.logId,
|
|
4314
4507
|
sizeBytes,
|
|
4315
4508
|
url: `${FOUND_BASE3}/${presign.key}`
|
|
4316
4509
|
};
|
|
@@ -4362,6 +4555,145 @@ var init_clips = __esm(() => {
|
|
|
4362
4555
|
MAX_CLIP_BYTES = 100 * 1024 * 1024;
|
|
4363
4556
|
});
|
|
4364
4557
|
|
|
4558
|
+
// src/commands/recordings.ts
|
|
4559
|
+
var exports_recordings2 = {};
|
|
4560
|
+
__export(exports_recordings2, {
|
|
4561
|
+
recordingsListCommand: () => recordingsListCommand2,
|
|
4562
|
+
recordingUpdateCommand: () => recordingUpdateCommand2,
|
|
4563
|
+
recordingPromoteCommand: () => recordingPromoteCommand2,
|
|
4564
|
+
recordingGetCommand: () => recordingGetCommand2,
|
|
4565
|
+
recordingGet: () => recordingGet2,
|
|
4566
|
+
recordingDeleteCommand: () => recordingDeleteCommand2,
|
|
4567
|
+
recordingCreateCommand: () => recordingCreateCommand2
|
|
4568
|
+
});
|
|
4569
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
|
|
4570
|
+
async function recordingGet2(id) {
|
|
4571
|
+
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4572
|
+
return response.recording;
|
|
4573
|
+
}
|
|
4574
|
+
function formatRecordingSummary2(recording) {
|
|
4575
|
+
const promoted = recording.logId ? ` → fluncle://${recording.logId}` : " (un-promoted)";
|
|
4576
|
+
const cues = recording.tracklist.length;
|
|
4577
|
+
return `${recording.id} ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
|
|
4578
|
+
}
|
|
4579
|
+
async function recordingsListCommand2(options = {}) {
|
|
4580
|
+
const response = await adminApiGet("/api/admin/recordings");
|
|
4581
|
+
if (options.json) {
|
|
4582
|
+
printJson2({ ok: true, recordings: response.recordings });
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4585
|
+
if (response.recordings.length === 0) {
|
|
4586
|
+
console.log("No recordings yet.");
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
console.log(response.recordings.map(formatRecordingSummary2).join(`
|
|
4590
|
+
`));
|
|
4591
|
+
}
|
|
4592
|
+
async function recordingGetCommand2(id, options = {}) {
|
|
4593
|
+
if (!id) {
|
|
4594
|
+
throw new CliError2("missing_id", "Missing recording id for: get");
|
|
4595
|
+
}
|
|
4596
|
+
const recording = await recordingGet2(id);
|
|
4597
|
+
if (options.json) {
|
|
4598
|
+
printJson2({ ok: true, recording });
|
|
4599
|
+
return;
|
|
4600
|
+
}
|
|
4601
|
+
console.log(formatRecordingSummary2(recording));
|
|
4602
|
+
console.log(` r2Key: ${recording.r2Key}`);
|
|
4603
|
+
for (const cue of recording.tracklist) {
|
|
4604
|
+
const at = cue.startMs === undefined ? "—" : `${(cue.startMs / 1000).toFixed(1)}s`;
|
|
4605
|
+
console.log(` ${at} ${cue.artists.join(", ")} — ${cue.title}`);
|
|
4606
|
+
}
|
|
4607
|
+
}
|
|
4608
|
+
async function recordingCreateCommand2(options = {}) {
|
|
4609
|
+
const title = options.title?.trim();
|
|
4610
|
+
if (!title) {
|
|
4611
|
+
throw new CliError2("missing_title", "A recording needs a --title");
|
|
4612
|
+
}
|
|
4613
|
+
if (!options.video) {
|
|
4614
|
+
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
4615
|
+
}
|
|
4616
|
+
if (!existsSync6(options.video)) {
|
|
4617
|
+
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
4618
|
+
}
|
|
4619
|
+
const created = await adminApiPost("/api/admin/recordings", {
|
|
4620
|
+
recordedAt: options.recordedAt,
|
|
4621
|
+
title
|
|
4622
|
+
});
|
|
4623
|
+
const recording = created.recording;
|
|
4624
|
+
const log = (message) => {
|
|
4625
|
+
if (!options.json) {
|
|
4626
|
+
console.log(message);
|
|
4627
|
+
}
|
|
4628
|
+
};
|
|
4629
|
+
log(`Recording ${recording.id} created — staging the set video…`);
|
|
4630
|
+
const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
|
|
4631
|
+
if (options.json) {
|
|
4632
|
+
printJson2({ key: upload.key, ok: true, recording, url: upload.url });
|
|
4633
|
+
return;
|
|
4634
|
+
}
|
|
4635
|
+
console.log(`Recording ${recording.id} staged → ${upload.url}`);
|
|
4636
|
+
}
|
|
4637
|
+
async function recordingUpdateCommand2(id, options = {}) {
|
|
4638
|
+
if (!id) {
|
|
4639
|
+
throw new CliError2("missing_id", "Missing recording id for: update");
|
|
4640
|
+
}
|
|
4641
|
+
const body = {};
|
|
4642
|
+
if (options.title !== undefined) {
|
|
4643
|
+
body.title = options.title;
|
|
4644
|
+
}
|
|
4645
|
+
if (options.recordedAt !== undefined) {
|
|
4646
|
+
body.recordedAt = options.recordedAt;
|
|
4647
|
+
}
|
|
4648
|
+
if (options.tracklistFile !== undefined) {
|
|
4649
|
+
if (!existsSync6(options.tracklistFile)) {
|
|
4650
|
+
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
4651
|
+
}
|
|
4652
|
+
try {
|
|
4653
|
+
body.tracklistJson = JSON.parse(readFileSync5(options.tracklistFile, "utf8"));
|
|
4654
|
+
} catch {
|
|
4655
|
+
throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
|
|
4656
|
+
}
|
|
4657
|
+
}
|
|
4658
|
+
if (Object.keys(body).length === 0) {
|
|
4659
|
+
throw new CliError2("no_fields", "Nothing to update — pass --title, --recorded-at, or --tracklist-file");
|
|
4660
|
+
}
|
|
4661
|
+
const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
|
|
4662
|
+
if (options.json) {
|
|
4663
|
+
printJson2({ ok: true, recording: response.recording });
|
|
4664
|
+
return;
|
|
4665
|
+
}
|
|
4666
|
+
console.log(`Updated ${formatRecordingSummary2(response.recording)}`);
|
|
4667
|
+
}
|
|
4668
|
+
async function recordingDeleteCommand2(id, options = {}) {
|
|
4669
|
+
if (!id) {
|
|
4670
|
+
throw new CliError2("missing_id", "Missing recording id for: delete");
|
|
4671
|
+
}
|
|
4672
|
+
await adminApiDelete(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4673
|
+
if (options.json) {
|
|
4674
|
+
printJson2({ ok: true });
|
|
4675
|
+
return;
|
|
4676
|
+
}
|
|
4677
|
+
console.log(`Deleted recording ${id} (and its clips).`);
|
|
4678
|
+
}
|
|
4679
|
+
async function recordingPromoteCommand2(id, options = {}) {
|
|
4680
|
+
if (!id) {
|
|
4681
|
+
throw new CliError2("missing_id", "Missing recording id for: promote");
|
|
4682
|
+
}
|
|
4683
|
+
const response = await adminApiPost(`/api/admin/recordings/${encodeURIComponent(id)}/promote`);
|
|
4684
|
+
const recording = response.recording;
|
|
4685
|
+
if (options.json) {
|
|
4686
|
+
printJson2({ ok: true, recording });
|
|
4687
|
+
return;
|
|
4688
|
+
}
|
|
4689
|
+
console.log(recording.logId ? `Promoted recording ${id} → mixtape fluncle://${recording.logId}` : `Promoted recording ${id}`);
|
|
4690
|
+
}
|
|
4691
|
+
var init_recordings2 = __esm(() => {
|
|
4692
|
+
init_api();
|
|
4693
|
+
init_output();
|
|
4694
|
+
init_mixtape_set_video();
|
|
4695
|
+
});
|
|
4696
|
+
|
|
4365
4697
|
// src/commands/newsletter.ts
|
|
4366
4698
|
var exports_newsletter = {};
|
|
4367
4699
|
__export(exports_newsletter, {
|
|
@@ -4371,7 +4703,7 @@ __export(exports_newsletter, {
|
|
|
4371
4703
|
newsletterDraftCommand: () => newsletterDraftCommand,
|
|
4372
4704
|
newsletterDeleteCommand: () => newsletterDeleteCommand
|
|
4373
4705
|
});
|
|
4374
|
-
import { existsSync as
|
|
4706
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
4375
4707
|
function buildBody3(options, { requireContent }) {
|
|
4376
4708
|
const body = {};
|
|
4377
4709
|
if (options.contentFile !== undefined) {
|
|
@@ -4391,10 +4723,10 @@ function buildBody3(options, { requireContent }) {
|
|
|
4391
4723
|
return body;
|
|
4392
4724
|
}
|
|
4393
4725
|
function readContentFile(filePath) {
|
|
4394
|
-
if (!
|
|
4726
|
+
if (!existsSync7(filePath)) {
|
|
4395
4727
|
throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
|
|
4396
4728
|
}
|
|
4397
|
-
const text =
|
|
4729
|
+
const text = readFileSync6(filePath, "utf-8");
|
|
4398
4730
|
try {
|
|
4399
4731
|
return JSON.parse(text);
|
|
4400
4732
|
} catch (error) {
|
|
@@ -5006,7 +5338,7 @@ function vehicleRows(rows) {
|
|
|
5006
5338
|
return Math.max(width, coordinate2(row).length);
|
|
5007
5339
|
}, 0);
|
|
5008
5340
|
return rows.map((row) => {
|
|
5009
|
-
return `${coordinate2(row).padEnd(coordWidth)} ${foundDate(row.addedAt)} ${row.vehicle ?? COORD_FALLBACK2}`;
|
|
5341
|
+
return `${coordinate2(row).padEnd(coordWidth)} ${foundDate(row.addedAt)} ${row.vehicle ?? COORD_FALLBACK2} · ${row.register ?? COORD_FALLBACK2}`;
|
|
5010
5342
|
});
|
|
5011
5343
|
}
|
|
5012
5344
|
function trackDetailLines(track) {
|
|
@@ -5029,7 +5361,7 @@ var init_format2 = __esm(() => {
|
|
|
5029
5361
|
});
|
|
5030
5362
|
|
|
5031
5363
|
// src/cli.ts
|
|
5032
|
-
import { existsSync as
|
|
5364
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
5033
5365
|
import path2 from "path";
|
|
5034
5366
|
|
|
5035
5367
|
// ../../node_modules/commander/lib/error.js
|
|
@@ -7397,7 +7729,7 @@ function addAdminCommands(program2) {
|
|
|
7397
7729
|
adminClips.action(() => {
|
|
7398
7730
|
adminClips.outputHelp();
|
|
7399
7731
|
});
|
|
7400
|
-
adminClips.command("list").description("List clips (filter by --status pending|done and/or --mixtape <id>)").option("--status <status>", "Filter by cut status (pending|done)").option("--mixtape <id>", "Filter by mixtape id").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7732
|
+
adminClips.command("list").description("List clips (filter by --status pending|done, --recording <id>, and/or --mixtape <id>)").option("--status <status>", "Filter by cut status (pending|done)").option("--recording <id>", "Filter by recording id").option("--mixtape <id>", "Filter by mixtape id").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7401
7733
|
const { clipsListCommand: clipsListCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7402
7734
|
await runClipsList(options, clipsListCommand2);
|
|
7403
7735
|
});
|
|
@@ -7405,6 +7737,34 @@ function addAdminCommands(program2) {
|
|
|
7405
7737
|
const { clipCutCommand: clipCutCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7406
7738
|
await runClipsCut(clipId, options, clipCutCommand2);
|
|
7407
7739
|
});
|
|
7740
|
+
const adminRecordings = configureCommand(admin.command("recordings").description("Recording (unpublished set) commands"));
|
|
7741
|
+
adminRecordings.action(() => {
|
|
7742
|
+
adminRecordings.outputHelp();
|
|
7743
|
+
});
|
|
7744
|
+
adminRecordings.command("create").description("Create a recording + stage its set video (from --video)").option("--title <text>", "Recording title").option("--video <file>", "Set-video master to stage (a 1080p rendition is derived)").option("--recorded-at <date>", "Recorded date (ISO)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7745
|
+
const { recordingCreateCommand: recordingCreateCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7746
|
+
await recordingCreateCommand3(options);
|
|
7747
|
+
});
|
|
7748
|
+
adminRecordings.command("list").description("List every recording").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7749
|
+
const { recordingsListCommand: recordingsListCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7750
|
+
await recordingsListCommand3(options);
|
|
7751
|
+
});
|
|
7752
|
+
adminRecordings.command("get").description("Show one recording by id").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7753
|
+
const { recordingGetCommand: recordingGetCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7754
|
+
await recordingGetCommand3(id, options);
|
|
7755
|
+
});
|
|
7756
|
+
adminRecordings.command("update").description("Update a recording's title, recorded date, or tracklist (--tracklist-file)").argument("[id]").option("--title <text>", "Recording title").option("--recorded-at <date>", "Recorded date (ISO)").option("--tracklist-file <file>", "JSON file with the whole cue tracklist array").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7757
|
+
const { recordingUpdateCommand: recordingUpdateCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7758
|
+
await recordingUpdateCommand3(id, options);
|
|
7759
|
+
});
|
|
7760
|
+
adminRecordings.command("delete").description("Delete a recording (cascade its clips)").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7761
|
+
const { recordingDeleteCommand: recordingDeleteCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7762
|
+
await recordingDeleteCommand3(id, options);
|
|
7763
|
+
});
|
|
7764
|
+
adminRecordings.command("promote").description("Promote a recording to a published mixtape (mint-or-reuse; idempotent)").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7765
|
+
const { recordingPromoteCommand: recordingPromoteCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7766
|
+
await recordingPromoteCommand3(id, options);
|
|
7767
|
+
});
|
|
7408
7768
|
const adminNewsletter = configureCommand(admin.command("newsletter").description("Newsletter edition commands"));
|
|
7409
7769
|
adminNewsletter.action(() => {
|
|
7410
7770
|
adminNewsletter.outputHelp();
|
|
@@ -7508,7 +7868,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
|
|
|
7508
7868
|
console.log(` mime: ${result.mime}`);
|
|
7509
7869
|
}
|
|
7510
7870
|
async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
|
|
7511
|
-
const script = options.scriptFile ?
|
|
7871
|
+
const script = options.scriptFile ? readFileSync7(options.scriptFile, "utf8") : options.script;
|
|
7512
7872
|
if (!idOrLogId || !script || !script.trim()) {
|
|
7513
7873
|
throw new Error("Usage: fluncle admin tracks observe <track_id|log_id> (--script <text> | --script-file <file>) [--voice-id <id>] [--duration-ms <ms>] [--context-note <text>] [--json]");
|
|
7514
7874
|
}
|
|
@@ -7564,7 +7924,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
|
|
|
7564
7924
|
}
|
|
7565
7925
|
}
|
|
7566
7926
|
async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
|
|
7567
|
-
const note = options.scriptFile ?
|
|
7927
|
+
const note = options.scriptFile ? readFileSync7(options.scriptFile, "utf8") : options.script;
|
|
7568
7928
|
if (!idOrLogId || !note || !note.trim()) {
|
|
7569
7929
|
throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
|
|
7570
7930
|
}
|
|
@@ -7710,7 +8070,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
|
|
|
7710
8070
|
return;
|
|
7711
8071
|
}
|
|
7712
8072
|
const candidate = path2.join(dir, name);
|
|
7713
|
-
return
|
|
8073
|
+
return existsSync8(candidate) ? candidate : undefined;
|
|
7714
8074
|
};
|
|
7715
8075
|
const resolveFile = (explicit, name) => {
|
|
7716
8076
|
if (explicit) {
|
|
@@ -7998,7 +8358,11 @@ async function runMixtapeList(options, mixtapeListCommand2) {
|
|
|
7998
8358
|
}
|
|
7999
8359
|
}
|
|
8000
8360
|
async function runClipsList(options, clipsListCommand2) {
|
|
8001
|
-
const clips = await clipsListCommand2({
|
|
8361
|
+
const clips = await clipsListCommand2({
|
|
8362
|
+
mixtapeId: options.mixtape,
|
|
8363
|
+
recordingId: options.recording,
|
|
8364
|
+
status: options.status
|
|
8365
|
+
});
|
|
8002
8366
|
if (options.json) {
|
|
8003
8367
|
printJson({ clips, ok: true });
|
|
8004
8368
|
return;
|
|
@@ -8008,7 +8372,8 @@ async function runClipsList(options, clipsListCommand2) {
|
|
|
8008
8372
|
return;
|
|
8009
8373
|
}
|
|
8010
8374
|
for (const clip of clips) {
|
|
8011
|
-
|
|
8375
|
+
const source = clip.recordingId ?? clip.mixtapeId ?? "\u2014";
|
|
8376
|
+
console.log(`${clip.id} ${clip.status} ${source} ${clip.inMs}-${clip.outMs}ms x=${clip.xOffset}`);
|
|
8012
8377
|
}
|
|
8013
8378
|
}
|
|
8014
8379
|
async function runClipsCut(clipId, options, clipCutCommand2) {
|
package/package.json
CHANGED