fluncle 0.78.0 → 0.79.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 +401 -40
- 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.79.0".trim() ? "0.79.0".trim() : "0.1.0";
|
|
562
562
|
});
|
|
563
563
|
|
|
564
564
|
// src/update-notifier.ts
|
|
@@ -1351,6 +1351,7 @@ var init_mixtape_mixcloud = __esm(() => {
|
|
|
1351
1351
|
// src/commands/mixtape-set-video.ts
|
|
1352
1352
|
var exports_mixtape_set_video = {};
|
|
1353
1353
|
__export(exports_mixtape_set_video, {
|
|
1354
|
+
uploadRenditionMultipart: () => uploadRenditionMultipart,
|
|
1354
1355
|
stageSetVideo: () => stageSetVideo,
|
|
1355
1356
|
renditionFfmpegArgs: () => renditionFfmpegArgs,
|
|
1356
1357
|
planMultipart: () => planMultipart,
|
|
@@ -1417,7 +1418,7 @@ function renditionFfmpegArgs(inputPath, outputPath) {
|
|
|
1417
1418
|
outputPath
|
|
1418
1419
|
];
|
|
1419
1420
|
}
|
|
1420
|
-
async function
|
|
1421
|
+
async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
|
|
1421
1422
|
if (!existsSync(masterPath)) {
|
|
1422
1423
|
throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
|
|
1423
1424
|
}
|
|
@@ -1429,7 +1430,9 @@ async function stageSetVideo(mixtapeId, masterPath, onProgress = () => {}) {
|
|
|
1429
1430
|
const size = statSync2(renditionPath).size;
|
|
1430
1431
|
const plan = planMultipart(size);
|
|
1431
1432
|
onProgress(`Set video: uploading ${(size / 1e9).toFixed(2)} GB in ${plan.partCount} part(s)…`);
|
|
1432
|
-
const presign = await adminApiPost(
|
|
1433
|
+
const presign = await adminApiPost(presignPath, {
|
|
1434
|
+
partCount: plan.partCount
|
|
1435
|
+
});
|
|
1433
1436
|
const urlByPart = new Map(presign.parts.map((part) => [part.partNumber, part.url]));
|
|
1434
1437
|
const completed = [];
|
|
1435
1438
|
try {
|
|
@@ -1447,13 +1450,19 @@ async function stageSetVideo(mixtapeId, masterPath, onProgress = () => {}) {
|
|
|
1447
1450
|
await abortUpload(presign.abortUrl).catch(() => {});
|
|
1448
1451
|
throw error;
|
|
1449
1452
|
}
|
|
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
1453
|
return { key: presign.key, url: `${FOUND_BASE}/${presign.key}` };
|
|
1453
1454
|
} finally {
|
|
1454
1455
|
rmSync2(renditionPath, { force: true });
|
|
1455
1456
|
}
|
|
1456
1457
|
}
|
|
1458
|
+
async function stageSetVideo(mixtapeId, masterPath, onProgress = () => {}) {
|
|
1459
|
+
const result = await uploadRenditionMultipart(masterPath, `/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/set-video/presign`, onProgress);
|
|
1460
|
+
await adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}`, {
|
|
1461
|
+
setVideoAt: new Date().toISOString()
|
|
1462
|
+
});
|
|
1463
|
+
onProgress("Set video: flipped setVideoAt — the /log player + video SEO are live.");
|
|
1464
|
+
return result;
|
|
1465
|
+
}
|
|
1457
1466
|
async function putPart(url, path2, part) {
|
|
1458
1467
|
const response = await fetch(url, {
|
|
1459
1468
|
body: Bun.file(path2).slice(part.start, part.end),
|
|
@@ -2455,7 +2464,7 @@ function parseVersion2(version) {
|
|
|
2455
2464
|
var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
|
|
2456
2465
|
var init_version2 = __esm(() => {
|
|
2457
2466
|
init_output();
|
|
2458
|
-
currentVersion2 = "0.
|
|
2467
|
+
currentVersion2 = "0.79.0".trim() ? "0.79.0".trim() : "0.1.0";
|
|
2459
2468
|
});
|
|
2460
2469
|
|
|
2461
2470
|
// ../../packages/registry/src/index.ts
|
|
@@ -3842,6 +3851,145 @@ var init_mixtape_youtube2 = __esm(() => {
|
|
|
3842
3851
|
init_output();
|
|
3843
3852
|
});
|
|
3844
3853
|
|
|
3854
|
+
// src/commands/recordings.ts
|
|
3855
|
+
var exports_recordings = {};
|
|
3856
|
+
__export(exports_recordings, {
|
|
3857
|
+
recordingsListCommand: () => recordingsListCommand,
|
|
3858
|
+
recordingUpdateCommand: () => recordingUpdateCommand,
|
|
3859
|
+
recordingPromoteCommand: () => recordingPromoteCommand,
|
|
3860
|
+
recordingGetCommand: () => recordingGetCommand,
|
|
3861
|
+
recordingGet: () => recordingGet,
|
|
3862
|
+
recordingDeleteCommand: () => recordingDeleteCommand,
|
|
3863
|
+
recordingCreateCommand: () => recordingCreateCommand
|
|
3864
|
+
});
|
|
3865
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
3866
|
+
async function recordingGet(id) {
|
|
3867
|
+
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
3868
|
+
return response.recording;
|
|
3869
|
+
}
|
|
3870
|
+
function formatRecordingSummary(recording) {
|
|
3871
|
+
const promoted = recording.logId ? ` → fluncle://${recording.logId}` : " (un-promoted)";
|
|
3872
|
+
const cues = recording.tracklist.length;
|
|
3873
|
+
return `${recording.id} ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
|
|
3874
|
+
}
|
|
3875
|
+
async function recordingsListCommand(options = {}) {
|
|
3876
|
+
const response = await adminApiGet("/api/admin/recordings");
|
|
3877
|
+
if (options.json) {
|
|
3878
|
+
printJson2({ ok: true, recordings: response.recordings });
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
if (response.recordings.length === 0) {
|
|
3882
|
+
console.log("No recordings yet.");
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
console.log(response.recordings.map(formatRecordingSummary).join(`
|
|
3886
|
+
`));
|
|
3887
|
+
}
|
|
3888
|
+
async function recordingGetCommand(id, options = {}) {
|
|
3889
|
+
if (!id) {
|
|
3890
|
+
throw new CliError2("missing_id", "Missing recording id for: get");
|
|
3891
|
+
}
|
|
3892
|
+
const recording = await recordingGet(id);
|
|
3893
|
+
if (options.json) {
|
|
3894
|
+
printJson2({ ok: true, recording });
|
|
3895
|
+
return;
|
|
3896
|
+
}
|
|
3897
|
+
console.log(formatRecordingSummary(recording));
|
|
3898
|
+
console.log(` r2Key: ${recording.r2Key}`);
|
|
3899
|
+
for (const cue of recording.tracklist) {
|
|
3900
|
+
const at = cue.startMs === undefined ? "—" : `${(cue.startMs / 1000).toFixed(1)}s`;
|
|
3901
|
+
console.log(` ${at} ${cue.artists.join(", ")} — ${cue.title}`);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
async function recordingCreateCommand(options = {}) {
|
|
3905
|
+
const title = options.title?.trim();
|
|
3906
|
+
if (!title) {
|
|
3907
|
+
throw new CliError2("missing_title", "A recording needs a --title");
|
|
3908
|
+
}
|
|
3909
|
+
if (!options.video) {
|
|
3910
|
+
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
3911
|
+
}
|
|
3912
|
+
if (!existsSync3(options.video)) {
|
|
3913
|
+
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
3914
|
+
}
|
|
3915
|
+
const created = await adminApiPost("/api/admin/recordings", {
|
|
3916
|
+
recordedAt: options.recordedAt,
|
|
3917
|
+
title
|
|
3918
|
+
});
|
|
3919
|
+
const recording = created.recording;
|
|
3920
|
+
const log = (message) => {
|
|
3921
|
+
if (!options.json) {
|
|
3922
|
+
console.log(message);
|
|
3923
|
+
}
|
|
3924
|
+
};
|
|
3925
|
+
log(`Recording ${recording.id} created — staging the set video…`);
|
|
3926
|
+
const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
|
|
3927
|
+
if (options.json) {
|
|
3928
|
+
printJson2({ key: upload.key, ok: true, recording, url: upload.url });
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3931
|
+
console.log(`Recording ${recording.id} staged → ${upload.url}`);
|
|
3932
|
+
}
|
|
3933
|
+
async function recordingUpdateCommand(id, options = {}) {
|
|
3934
|
+
if (!id) {
|
|
3935
|
+
throw new CliError2("missing_id", "Missing recording id for: update");
|
|
3936
|
+
}
|
|
3937
|
+
const body = {};
|
|
3938
|
+
if (options.title !== undefined) {
|
|
3939
|
+
body.title = options.title;
|
|
3940
|
+
}
|
|
3941
|
+
if (options.recordedAt !== undefined) {
|
|
3942
|
+
body.recordedAt = options.recordedAt;
|
|
3943
|
+
}
|
|
3944
|
+
if (options.tracklistFile !== undefined) {
|
|
3945
|
+
if (!existsSync3(options.tracklistFile)) {
|
|
3946
|
+
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
3947
|
+
}
|
|
3948
|
+
try {
|
|
3949
|
+
body.tracklistJson = JSON.parse(readFileSync3(options.tracklistFile, "utf8"));
|
|
3950
|
+
} catch {
|
|
3951
|
+
throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
if (Object.keys(body).length === 0) {
|
|
3955
|
+
throw new CliError2("no_fields", "Nothing to update — pass --title, --recorded-at, or --tracklist-file");
|
|
3956
|
+
}
|
|
3957
|
+
const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
|
|
3958
|
+
if (options.json) {
|
|
3959
|
+
printJson2({ ok: true, recording: response.recording });
|
|
3960
|
+
return;
|
|
3961
|
+
}
|
|
3962
|
+
console.log(`Updated ${formatRecordingSummary(response.recording)}`);
|
|
3963
|
+
}
|
|
3964
|
+
async function recordingDeleteCommand(id, options = {}) {
|
|
3965
|
+
if (!id) {
|
|
3966
|
+
throw new CliError2("missing_id", "Missing recording id for: delete");
|
|
3967
|
+
}
|
|
3968
|
+
await adminApiDelete(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
3969
|
+
if (options.json) {
|
|
3970
|
+
printJson2({ ok: true });
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
console.log(`Deleted recording ${id} (and its clips).`);
|
|
3974
|
+
}
|
|
3975
|
+
async function recordingPromoteCommand(id, options = {}) {
|
|
3976
|
+
if (!id) {
|
|
3977
|
+
throw new CliError2("missing_id", "Missing recording id for: promote");
|
|
3978
|
+
}
|
|
3979
|
+
const response = await adminApiPost(`/api/admin/recordings/${encodeURIComponent(id)}/promote`);
|
|
3980
|
+
const recording = response.recording;
|
|
3981
|
+
if (options.json) {
|
|
3982
|
+
printJson2({ ok: true, recording });
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3985
|
+
console.log(recording.logId ? `Promoted recording ${id} → mixtape fluncle://${recording.logId}` : `Promoted recording ${id}`);
|
|
3986
|
+
}
|
|
3987
|
+
var init_recordings = __esm(() => {
|
|
3988
|
+
init_api();
|
|
3989
|
+
init_output();
|
|
3990
|
+
init_mixtape_set_video();
|
|
3991
|
+
});
|
|
3992
|
+
|
|
3845
3993
|
// src/commands/mixtapes.ts
|
|
3846
3994
|
var exports_mixtapes2 = {};
|
|
3847
3995
|
__export(exports_mixtapes2, {
|
|
@@ -3856,7 +4004,7 @@ __export(exports_mixtapes2, {
|
|
|
3856
4004
|
mixtapeDeleteCommand: () => mixtapeDeleteCommand2,
|
|
3857
4005
|
mixtapeCreateCommand: () => mixtapeCreateCommand2
|
|
3858
4006
|
});
|
|
3859
|
-
import { existsSync as
|
|
4007
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
3860
4008
|
async function mixtapesCommand2() {
|
|
3861
4009
|
const response = await publicApiGet("/api/mixtapes");
|
|
3862
4010
|
return response.mixtapes;
|
|
@@ -4024,10 +4172,10 @@ function buildBody2(options) {
|
|
|
4024
4172
|
return body;
|
|
4025
4173
|
}
|
|
4026
4174
|
function parseCueFile2(filePath) {
|
|
4027
|
-
if (!
|
|
4175
|
+
if (!existsSync4(filePath)) {
|
|
4028
4176
|
throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
|
|
4029
4177
|
}
|
|
4030
|
-
const text =
|
|
4178
|
+
const text = readFileSync4(filePath, "utf-8");
|
|
4031
4179
|
const trimmed = text.trim();
|
|
4032
4180
|
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
4033
4181
|
try {
|
|
@@ -4124,7 +4272,7 @@ __export(exports_clips, {
|
|
|
4124
4272
|
BOX_CLIP_FONT_FILE: () => BOX_CLIP_FONT_FILE
|
|
4125
4273
|
});
|
|
4126
4274
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4127
|
-
import { existsSync as
|
|
4275
|
+
import { existsSync as existsSync5, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
|
|
4128
4276
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4129
4277
|
import { join as join5 } from "node:path";
|
|
4130
4278
|
function clipFootageKey(clipId) {
|
|
@@ -4135,7 +4283,7 @@ function resolveFontFile(envVar, bakedPath) {
|
|
|
4135
4283
|
if (explicit) {
|
|
4136
4284
|
return explicit;
|
|
4137
4285
|
}
|
|
4138
|
-
return
|
|
4286
|
+
return existsSync5(bakedPath) ? bakedPath : undefined;
|
|
4139
4287
|
}
|
|
4140
4288
|
function resolveClipFontFile() {
|
|
4141
4289
|
return resolveFontFile("CLIP_FONT_FILE", BOX_CLIP_FONT_FILE);
|
|
@@ -4185,24 +4333,32 @@ function resolveTitleLines(options, y) {
|
|
|
4185
4333
|
function clipCutFilterComplex(options) {
|
|
4186
4334
|
const xOffset = Math.max(0, Math.round(options.xOffset));
|
|
4187
4335
|
const crop = `crop=ih*9/16:ih:${xOffset}:0,scale=${CLIP_WIDTH}:${CLIP_HEIGHT}`;
|
|
4336
|
+
const logId = options.logId?.trim();
|
|
4337
|
+
const hasCoordinate = Boolean(logId);
|
|
4188
4338
|
const coordinateBox = Math.round(CLIP_COORDINATE_SIZE * 1.18);
|
|
4189
|
-
const titleBottomOffset = CLIP_SAFE_BOTTOM + coordinateBox + CLIP_LINE_GAP;
|
|
4339
|
+
const titleBottomOffset = hasCoordinate ? CLIP_SAFE_BOTTOM + coordinateBox + CLIP_LINE_GAP : CLIP_SAFE_BOTTOM;
|
|
4190
4340
|
const titleY = `h-${titleBottomOffset}-th`;
|
|
4191
4341
|
const titleLines = resolveTitleLines(options, titleY);
|
|
4192
|
-
const coordinate2 = {
|
|
4342
|
+
const coordinate2 = hasCoordinate ? {
|
|
4193
4343
|
fontFile: options.oxaniumFontFile,
|
|
4194
4344
|
size: CLIP_COORDINATE_SIZE,
|
|
4195
|
-
text: `fluncle://${
|
|
4345
|
+
text: `fluncle://${logId}`,
|
|
4196
4346
|
x: CLIP_MARGIN_X,
|
|
4197
4347
|
y: `h-${CLIP_SAFE_BOTTOM}-th`
|
|
4198
|
-
};
|
|
4348
|
+
} : undefined;
|
|
4199
4349
|
const haloGlyphs = [
|
|
4200
4350
|
...titleLines.map((line) => brandDrawtext({ ...line, color: CLIP_HALO_COLOR })),
|
|
4201
|
-
brandDrawtext({ ...coordinate2, color: CLIP_HALO_COLOR })
|
|
4351
|
+
...coordinate2 ? [brandDrawtext({ ...coordinate2, color: CLIP_HALO_COLOR })] : []
|
|
4202
4352
|
].join(",");
|
|
4203
4353
|
const sharpGlyphs = [
|
|
4204
4354
|
...titleLines.map((line) => brandDrawtext({ ...line, borderw: CLIP_SHARP_BORDERW, color: CLIP_TITLE_COLOR })),
|
|
4205
|
-
|
|
4355
|
+
...coordinate2 ? [
|
|
4356
|
+
brandDrawtext({
|
|
4357
|
+
...coordinate2,
|
|
4358
|
+
borderw: CLIP_SHARP_BORDERW,
|
|
4359
|
+
color: CLIP_COORDINATE_COLOR
|
|
4360
|
+
})
|
|
4361
|
+
] : []
|
|
4206
4362
|
].join(",");
|
|
4207
4363
|
return [
|
|
4208
4364
|
`[0:v]${crop},setsar=1[base]`,
|
|
@@ -4256,6 +4412,9 @@ function clipCutFfmpegArgs(options) {
|
|
|
4256
4412
|
}
|
|
4257
4413
|
async function clipsListCommand(filter = {}) {
|
|
4258
4414
|
const params = new URLSearchParams;
|
|
4415
|
+
if (filter.recordingId) {
|
|
4416
|
+
params.set("recordingId", filter.recordingId);
|
|
4417
|
+
}
|
|
4259
4418
|
if (filter.mixtapeId) {
|
|
4260
4419
|
params.set("mixtapeId", filter.mixtapeId);
|
|
4261
4420
|
}
|
|
@@ -4266,11 +4425,27 @@ async function clipsListCommand(filter = {}) {
|
|
|
4266
4425
|
const response = await adminApiGet(`/api/admin/clips${query ? `?${query}` : ""}`);
|
|
4267
4426
|
return response.clips;
|
|
4268
4427
|
}
|
|
4269
|
-
async function
|
|
4270
|
-
|
|
4271
|
-
|
|
4272
|
-
|
|
4273
|
-
|
|
4428
|
+
async function resolveClipSource(clip) {
|
|
4429
|
+
if (clip.recordingId) {
|
|
4430
|
+
const { recordingGet: recordingGet2 } = await Promise.resolve().then(() => (init_recordings(), exports_recordings));
|
|
4431
|
+
const recording = await recordingGet2(clip.recordingId);
|
|
4432
|
+
if (!recording.r2Key) {
|
|
4433
|
+
throw new CliError2("recording_not_staged", `Recording ${clip.recordingId} has no staged set video`);
|
|
4434
|
+
}
|
|
4435
|
+
return {
|
|
4436
|
+
logId: recording.logId,
|
|
4437
|
+
members: recording.tracklist.map((cue) => ({
|
|
4438
|
+
artists: cue.artists,
|
|
4439
|
+
startMs: cue.startMs,
|
|
4440
|
+
title: cue.title
|
|
4441
|
+
})),
|
|
4442
|
+
setDurationMs: recording.durationMs ?? 0,
|
|
4443
|
+
setUrl: `${FOUND_BASE3}/${recording.r2Key.split("/").map(encodeURIComponent).join("/")}`,
|
|
4444
|
+
title: recording.title
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
if (!clip.mixtapeId) {
|
|
4448
|
+
throw new CliError2("clip_unlinked", `Clip ${clip.id} is linked to neither a recording nor a mixtape`);
|
|
4274
4449
|
}
|
|
4275
4450
|
const { mixtapeGetCommand: mixtapeGetCommand2 } = await Promise.resolve().then(() => (init_mixtapes2(), exports_mixtapes2));
|
|
4276
4451
|
const mixtape = await mixtapeGetCommand2(clip.mixtapeId);
|
|
@@ -4280,22 +4455,36 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4280
4455
|
if (!mixtape.setVideoAt) {
|
|
4281
4456
|
throw new CliError2("set_not_staged", `Mixtape ${mixtape.logId} has no staged set video — run \`distribute --set-video\` first`);
|
|
4282
4457
|
}
|
|
4458
|
+
return {
|
|
4459
|
+
logId: mixtape.logId,
|
|
4460
|
+
members: mixtape.members,
|
|
4461
|
+
setDurationMs: mixtape.durationMs ?? 0,
|
|
4462
|
+
setUrl: setVideoUrl(mixtape.logId),
|
|
4463
|
+
title: mixtape.title
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
4467
|
+
const clips = await clipsListCommand();
|
|
4468
|
+
const clip = clips.find((candidate) => candidate.id === clipId);
|
|
4469
|
+
if (!clip) {
|
|
4470
|
+
throw new CliError2("clip_not_found", `No clip with id ${clipId}`);
|
|
4471
|
+
}
|
|
4472
|
+
const source = await resolveClipSource(clip);
|
|
4283
4473
|
await assertFfmpeg2();
|
|
4284
|
-
const setUrl = setVideoUrl(mixtape.logId);
|
|
4285
4474
|
const outputPath = join5(tmpdir2(), `fluncle-clip-${randomUUID2()}.mp4`);
|
|
4286
4475
|
try {
|
|
4287
|
-
onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms] from ${
|
|
4476
|
+
onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms]${source.logId ? ` from ${source.logId}` : ""}…`);
|
|
4288
4477
|
await runClipCut({
|
|
4289
4478
|
inMs: clip.inMs,
|
|
4290
|
-
logId:
|
|
4291
|
-
members:
|
|
4479
|
+
logId: source.logId,
|
|
4480
|
+
members: source.members,
|
|
4292
4481
|
outMs: clip.outMs,
|
|
4293
4482
|
outputPath,
|
|
4294
4483
|
oxaniumFontFile: resolveClipFontFile(),
|
|
4295
4484
|
sansFontFile: resolveClipSansFontFile(),
|
|
4296
|
-
setDurationMs:
|
|
4297
|
-
setUrl,
|
|
4298
|
-
title:
|
|
4485
|
+
setDurationMs: source.setDurationMs,
|
|
4486
|
+
setUrl: source.setUrl,
|
|
4487
|
+
title: source.title,
|
|
4299
4488
|
xOffset: clip.xOffset
|
|
4300
4489
|
});
|
|
4301
4490
|
const sizeBytes = statSync4(outputPath).size;
|
|
@@ -4310,7 +4499,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4310
4499
|
return {
|
|
4311
4500
|
clipId,
|
|
4312
4501
|
key: presign.key,
|
|
4313
|
-
logId:
|
|
4502
|
+
logId: source.logId,
|
|
4314
4503
|
sizeBytes,
|
|
4315
4504
|
url: `${FOUND_BASE3}/${presign.key}`
|
|
4316
4505
|
};
|
|
@@ -4362,6 +4551,145 @@ var init_clips = __esm(() => {
|
|
|
4362
4551
|
MAX_CLIP_BYTES = 100 * 1024 * 1024;
|
|
4363
4552
|
});
|
|
4364
4553
|
|
|
4554
|
+
// src/commands/recordings.ts
|
|
4555
|
+
var exports_recordings2 = {};
|
|
4556
|
+
__export(exports_recordings2, {
|
|
4557
|
+
recordingsListCommand: () => recordingsListCommand2,
|
|
4558
|
+
recordingUpdateCommand: () => recordingUpdateCommand2,
|
|
4559
|
+
recordingPromoteCommand: () => recordingPromoteCommand2,
|
|
4560
|
+
recordingGetCommand: () => recordingGetCommand2,
|
|
4561
|
+
recordingGet: () => recordingGet2,
|
|
4562
|
+
recordingDeleteCommand: () => recordingDeleteCommand2,
|
|
4563
|
+
recordingCreateCommand: () => recordingCreateCommand2
|
|
4564
|
+
});
|
|
4565
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
|
|
4566
|
+
async function recordingGet2(id) {
|
|
4567
|
+
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4568
|
+
return response.recording;
|
|
4569
|
+
}
|
|
4570
|
+
function formatRecordingSummary2(recording) {
|
|
4571
|
+
const promoted = recording.logId ? ` → fluncle://${recording.logId}` : " (un-promoted)";
|
|
4572
|
+
const cues = recording.tracklist.length;
|
|
4573
|
+
return `${recording.id} ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
|
|
4574
|
+
}
|
|
4575
|
+
async function recordingsListCommand2(options = {}) {
|
|
4576
|
+
const response = await adminApiGet("/api/admin/recordings");
|
|
4577
|
+
if (options.json) {
|
|
4578
|
+
printJson2({ ok: true, recordings: response.recordings });
|
|
4579
|
+
return;
|
|
4580
|
+
}
|
|
4581
|
+
if (response.recordings.length === 0) {
|
|
4582
|
+
console.log("No recordings yet.");
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4585
|
+
console.log(response.recordings.map(formatRecordingSummary2).join(`
|
|
4586
|
+
`));
|
|
4587
|
+
}
|
|
4588
|
+
async function recordingGetCommand2(id, options = {}) {
|
|
4589
|
+
if (!id) {
|
|
4590
|
+
throw new CliError2("missing_id", "Missing recording id for: get");
|
|
4591
|
+
}
|
|
4592
|
+
const recording = await recordingGet2(id);
|
|
4593
|
+
if (options.json) {
|
|
4594
|
+
printJson2({ ok: true, recording });
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
console.log(formatRecordingSummary2(recording));
|
|
4598
|
+
console.log(` r2Key: ${recording.r2Key}`);
|
|
4599
|
+
for (const cue of recording.tracklist) {
|
|
4600
|
+
const at = cue.startMs === undefined ? "—" : `${(cue.startMs / 1000).toFixed(1)}s`;
|
|
4601
|
+
console.log(` ${at} ${cue.artists.join(", ")} — ${cue.title}`);
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
async function recordingCreateCommand2(options = {}) {
|
|
4605
|
+
const title = options.title?.trim();
|
|
4606
|
+
if (!title) {
|
|
4607
|
+
throw new CliError2("missing_title", "A recording needs a --title");
|
|
4608
|
+
}
|
|
4609
|
+
if (!options.video) {
|
|
4610
|
+
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
4611
|
+
}
|
|
4612
|
+
if (!existsSync6(options.video)) {
|
|
4613
|
+
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
4614
|
+
}
|
|
4615
|
+
const created = await adminApiPost("/api/admin/recordings", {
|
|
4616
|
+
recordedAt: options.recordedAt,
|
|
4617
|
+
title
|
|
4618
|
+
});
|
|
4619
|
+
const recording = created.recording;
|
|
4620
|
+
const log = (message) => {
|
|
4621
|
+
if (!options.json) {
|
|
4622
|
+
console.log(message);
|
|
4623
|
+
}
|
|
4624
|
+
};
|
|
4625
|
+
log(`Recording ${recording.id} created — staging the set video…`);
|
|
4626
|
+
const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
|
|
4627
|
+
if (options.json) {
|
|
4628
|
+
printJson2({ key: upload.key, ok: true, recording, url: upload.url });
|
|
4629
|
+
return;
|
|
4630
|
+
}
|
|
4631
|
+
console.log(`Recording ${recording.id} staged → ${upload.url}`);
|
|
4632
|
+
}
|
|
4633
|
+
async function recordingUpdateCommand2(id, options = {}) {
|
|
4634
|
+
if (!id) {
|
|
4635
|
+
throw new CliError2("missing_id", "Missing recording id for: update");
|
|
4636
|
+
}
|
|
4637
|
+
const body = {};
|
|
4638
|
+
if (options.title !== undefined) {
|
|
4639
|
+
body.title = options.title;
|
|
4640
|
+
}
|
|
4641
|
+
if (options.recordedAt !== undefined) {
|
|
4642
|
+
body.recordedAt = options.recordedAt;
|
|
4643
|
+
}
|
|
4644
|
+
if (options.tracklistFile !== undefined) {
|
|
4645
|
+
if (!existsSync6(options.tracklistFile)) {
|
|
4646
|
+
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
4647
|
+
}
|
|
4648
|
+
try {
|
|
4649
|
+
body.tracklistJson = JSON.parse(readFileSync5(options.tracklistFile, "utf8"));
|
|
4650
|
+
} catch {
|
|
4651
|
+
throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
if (Object.keys(body).length === 0) {
|
|
4655
|
+
throw new CliError2("no_fields", "Nothing to update — pass --title, --recorded-at, or --tracklist-file");
|
|
4656
|
+
}
|
|
4657
|
+
const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
|
|
4658
|
+
if (options.json) {
|
|
4659
|
+
printJson2({ ok: true, recording: response.recording });
|
|
4660
|
+
return;
|
|
4661
|
+
}
|
|
4662
|
+
console.log(`Updated ${formatRecordingSummary2(response.recording)}`);
|
|
4663
|
+
}
|
|
4664
|
+
async function recordingDeleteCommand2(id, options = {}) {
|
|
4665
|
+
if (!id) {
|
|
4666
|
+
throw new CliError2("missing_id", "Missing recording id for: delete");
|
|
4667
|
+
}
|
|
4668
|
+
await adminApiDelete(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4669
|
+
if (options.json) {
|
|
4670
|
+
printJson2({ ok: true });
|
|
4671
|
+
return;
|
|
4672
|
+
}
|
|
4673
|
+
console.log(`Deleted recording ${id} (and its clips).`);
|
|
4674
|
+
}
|
|
4675
|
+
async function recordingPromoteCommand2(id, options = {}) {
|
|
4676
|
+
if (!id) {
|
|
4677
|
+
throw new CliError2("missing_id", "Missing recording id for: promote");
|
|
4678
|
+
}
|
|
4679
|
+
const response = await adminApiPost(`/api/admin/recordings/${encodeURIComponent(id)}/promote`);
|
|
4680
|
+
const recording = response.recording;
|
|
4681
|
+
if (options.json) {
|
|
4682
|
+
printJson2({ ok: true, recording });
|
|
4683
|
+
return;
|
|
4684
|
+
}
|
|
4685
|
+
console.log(recording.logId ? `Promoted recording ${id} → mixtape fluncle://${recording.logId}` : `Promoted recording ${id}`);
|
|
4686
|
+
}
|
|
4687
|
+
var init_recordings2 = __esm(() => {
|
|
4688
|
+
init_api();
|
|
4689
|
+
init_output();
|
|
4690
|
+
init_mixtape_set_video();
|
|
4691
|
+
});
|
|
4692
|
+
|
|
4365
4693
|
// src/commands/newsletter.ts
|
|
4366
4694
|
var exports_newsletter = {};
|
|
4367
4695
|
__export(exports_newsletter, {
|
|
@@ -4371,7 +4699,7 @@ __export(exports_newsletter, {
|
|
|
4371
4699
|
newsletterDraftCommand: () => newsletterDraftCommand,
|
|
4372
4700
|
newsletterDeleteCommand: () => newsletterDeleteCommand
|
|
4373
4701
|
});
|
|
4374
|
-
import { existsSync as
|
|
4702
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
4375
4703
|
function buildBody3(options, { requireContent }) {
|
|
4376
4704
|
const body = {};
|
|
4377
4705
|
if (options.contentFile !== undefined) {
|
|
@@ -4391,10 +4719,10 @@ function buildBody3(options, { requireContent }) {
|
|
|
4391
4719
|
return body;
|
|
4392
4720
|
}
|
|
4393
4721
|
function readContentFile(filePath) {
|
|
4394
|
-
if (!
|
|
4722
|
+
if (!existsSync7(filePath)) {
|
|
4395
4723
|
throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
|
|
4396
4724
|
}
|
|
4397
|
-
const text =
|
|
4725
|
+
const text = readFileSync6(filePath, "utf-8");
|
|
4398
4726
|
try {
|
|
4399
4727
|
return JSON.parse(text);
|
|
4400
4728
|
} catch (error) {
|
|
@@ -5029,7 +5357,7 @@ var init_format2 = __esm(() => {
|
|
|
5029
5357
|
});
|
|
5030
5358
|
|
|
5031
5359
|
// src/cli.ts
|
|
5032
|
-
import { existsSync as
|
|
5360
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
5033
5361
|
import path2 from "path";
|
|
5034
5362
|
|
|
5035
5363
|
// ../../node_modules/commander/lib/error.js
|
|
@@ -7397,7 +7725,7 @@ function addAdminCommands(program2) {
|
|
|
7397
7725
|
adminClips.action(() => {
|
|
7398
7726
|
adminClips.outputHelp();
|
|
7399
7727
|
});
|
|
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) => {
|
|
7728
|
+
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
7729
|
const { clipsListCommand: clipsListCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7402
7730
|
await runClipsList(options, clipsListCommand2);
|
|
7403
7731
|
});
|
|
@@ -7405,6 +7733,34 @@ function addAdminCommands(program2) {
|
|
|
7405
7733
|
const { clipCutCommand: clipCutCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7406
7734
|
await runClipsCut(clipId, options, clipCutCommand2);
|
|
7407
7735
|
});
|
|
7736
|
+
const adminRecordings = configureCommand(admin.command("recordings").description("Recording (unpublished set) commands"));
|
|
7737
|
+
adminRecordings.action(() => {
|
|
7738
|
+
adminRecordings.outputHelp();
|
|
7739
|
+
});
|
|
7740
|
+
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) => {
|
|
7741
|
+
const { recordingCreateCommand: recordingCreateCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7742
|
+
await recordingCreateCommand3(options);
|
|
7743
|
+
});
|
|
7744
|
+
adminRecordings.command("list").description("List every recording").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7745
|
+
const { recordingsListCommand: recordingsListCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7746
|
+
await recordingsListCommand3(options);
|
|
7747
|
+
});
|
|
7748
|
+
adminRecordings.command("get").description("Show one recording by id").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7749
|
+
const { recordingGetCommand: recordingGetCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7750
|
+
await recordingGetCommand3(id, options);
|
|
7751
|
+
});
|
|
7752
|
+
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) => {
|
|
7753
|
+
const { recordingUpdateCommand: recordingUpdateCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7754
|
+
await recordingUpdateCommand3(id, options);
|
|
7755
|
+
});
|
|
7756
|
+
adminRecordings.command("delete").description("Delete a recording (cascade its clips)").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7757
|
+
const { recordingDeleteCommand: recordingDeleteCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7758
|
+
await recordingDeleteCommand3(id, options);
|
|
7759
|
+
});
|
|
7760
|
+
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) => {
|
|
7761
|
+
const { recordingPromoteCommand: recordingPromoteCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7762
|
+
await recordingPromoteCommand3(id, options);
|
|
7763
|
+
});
|
|
7408
7764
|
const adminNewsletter = configureCommand(admin.command("newsletter").description("Newsletter edition commands"));
|
|
7409
7765
|
adminNewsletter.action(() => {
|
|
7410
7766
|
adminNewsletter.outputHelp();
|
|
@@ -7508,7 +7864,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
|
|
|
7508
7864
|
console.log(` mime: ${result.mime}`);
|
|
7509
7865
|
}
|
|
7510
7866
|
async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
|
|
7511
|
-
const script = options.scriptFile ?
|
|
7867
|
+
const script = options.scriptFile ? readFileSync7(options.scriptFile, "utf8") : options.script;
|
|
7512
7868
|
if (!idOrLogId || !script || !script.trim()) {
|
|
7513
7869
|
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
7870
|
}
|
|
@@ -7564,7 +7920,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
|
|
|
7564
7920
|
}
|
|
7565
7921
|
}
|
|
7566
7922
|
async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
|
|
7567
|
-
const note = options.scriptFile ?
|
|
7923
|
+
const note = options.scriptFile ? readFileSync7(options.scriptFile, "utf8") : options.script;
|
|
7568
7924
|
if (!idOrLogId || !note || !note.trim()) {
|
|
7569
7925
|
throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
|
|
7570
7926
|
}
|
|
@@ -7710,7 +8066,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
|
|
|
7710
8066
|
return;
|
|
7711
8067
|
}
|
|
7712
8068
|
const candidate = path2.join(dir, name);
|
|
7713
|
-
return
|
|
8069
|
+
return existsSync8(candidate) ? candidate : undefined;
|
|
7714
8070
|
};
|
|
7715
8071
|
const resolveFile = (explicit, name) => {
|
|
7716
8072
|
if (explicit) {
|
|
@@ -7998,7 +8354,11 @@ async function runMixtapeList(options, mixtapeListCommand2) {
|
|
|
7998
8354
|
}
|
|
7999
8355
|
}
|
|
8000
8356
|
async function runClipsList(options, clipsListCommand2) {
|
|
8001
|
-
const clips = await clipsListCommand2({
|
|
8357
|
+
const clips = await clipsListCommand2({
|
|
8358
|
+
mixtapeId: options.mixtape,
|
|
8359
|
+
recordingId: options.recording,
|
|
8360
|
+
status: options.status
|
|
8361
|
+
});
|
|
8002
8362
|
if (options.json) {
|
|
8003
8363
|
printJson({ clips, ok: true });
|
|
8004
8364
|
return;
|
|
@@ -8008,7 +8368,8 @@ async function runClipsList(options, clipsListCommand2) {
|
|
|
8008
8368
|
return;
|
|
8009
8369
|
}
|
|
8010
8370
|
for (const clip of clips) {
|
|
8011
|
-
|
|
8371
|
+
const source = clip.recordingId ?? clip.mixtapeId ?? "\u2014";
|
|
8372
|
+
console.log(`${clip.id} ${clip.status} ${source} ${clip.inMs}-${clip.outMs}ms x=${clip.xOffset}`);
|
|
8012
8373
|
}
|
|
8013
8374
|
}
|
|
8014
8375
|
async function runClipsCut(clipId, options, clipCutCommand2) {
|
package/package.json
CHANGED