fluncle 0.85.0 → 0.87.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 +303 -600
- package/package.json +1 -1
package/bin/fluncle.mjs
CHANGED
|
@@ -538,7 +538,7 @@ function parseVersion(version) {
|
|
|
538
538
|
var currentVersion;
|
|
539
539
|
var init_version = __esm(() => {
|
|
540
540
|
init_output();
|
|
541
|
-
currentVersion = "0.
|
|
541
|
+
currentVersion = "0.87.0".trim() ? "0.87.0".trim() : "0.1.0";
|
|
542
542
|
});
|
|
543
543
|
|
|
544
544
|
// src/update-notifier.ts
|
|
@@ -1329,186 +1329,6 @@ var init_mixtape_mixcloud = __esm(() => {
|
|
|
1329
1329
|
PICTURE_MAX_BYTES = 10 * 1024 * 1024;
|
|
1330
1330
|
});
|
|
1331
1331
|
|
|
1332
|
-
// src/commands/mixtape-set-video.ts
|
|
1333
|
-
var exports_mixtape_set_video = {};
|
|
1334
|
-
__export(exports_mixtape_set_video, {
|
|
1335
|
-
uploadRenditionMultipart: () => uploadRenditionMultipart,
|
|
1336
|
-
stageSetVideo: () => stageSetVideo,
|
|
1337
|
-
renditionFfmpegArgs: () => renditionFfmpegArgs,
|
|
1338
|
-
planMultipart: () => planMultipart,
|
|
1339
|
-
buildCompleteXml: () => buildCompleteXml,
|
|
1340
|
-
SET_VIDEO_RENDITION: () => SET_VIDEO_RENDITION,
|
|
1341
|
-
MIN_PART_SIZE: () => MIN_PART_SIZE,
|
|
1342
|
-
MAX_PARTS: () => MAX_PARTS,
|
|
1343
|
-
DEFAULT_PART_SIZE: () => DEFAULT_PART_SIZE
|
|
1344
|
-
});
|
|
1345
|
-
import { randomUUID } from "node:crypto";
|
|
1346
|
-
import { existsSync, rmSync as rmSync2, statSync as statSync2 } from "node:fs";
|
|
1347
|
-
import { tmpdir } from "node:os";
|
|
1348
|
-
import { join as join4 } from "node:path";
|
|
1349
|
-
function planMultipart(contentLength, partSize = DEFAULT_PART_SIZE) {
|
|
1350
|
-
if (!Number.isInteger(contentLength) || contentLength <= 0) {
|
|
1351
|
-
throw new CliError2("invalid_size", `The rendition size must be a positive integer (got ${contentLength})`);
|
|
1352
|
-
}
|
|
1353
|
-
let effective = Math.max(partSize, MIN_PART_SIZE);
|
|
1354
|
-
if (Math.ceil(contentLength / effective) > MAX_PARTS) {
|
|
1355
|
-
effective = Math.ceil(contentLength / MAX_PARTS);
|
|
1356
|
-
}
|
|
1357
|
-
const parts = [];
|
|
1358
|
-
let start = 0;
|
|
1359
|
-
let partNumber = 1;
|
|
1360
|
-
while (start < contentLength) {
|
|
1361
|
-
const end = Math.min(start + effective, contentLength);
|
|
1362
|
-
parts.push({ end, partNumber, size: end - start, start });
|
|
1363
|
-
start = end;
|
|
1364
|
-
partNumber += 1;
|
|
1365
|
-
}
|
|
1366
|
-
return { partCount: parts.length, partSize: effective, parts };
|
|
1367
|
-
}
|
|
1368
|
-
function escapeXml(value) {
|
|
1369
|
-
return value.replace(/[<>&'"]/g, (char) => ({ '"': """, "&": "&", "'": "'", "<": "<", ">": ">" })[char] ?? char);
|
|
1370
|
-
}
|
|
1371
|
-
function buildCompleteXml(parts) {
|
|
1372
|
-
const ordered = [...parts].sort((a, b) => a.partNumber - b.partNumber);
|
|
1373
|
-
const body = ordered.map((part) => `<Part><PartNumber>${part.partNumber}</PartNumber><ETag>${escapeXml(part.etag)}</ETag></Part>`).join("");
|
|
1374
|
-
return `<CompleteMultipartUpload>${body}</CompleteMultipartUpload>`;
|
|
1375
|
-
}
|
|
1376
|
-
function renditionFfmpegArgs(inputPath, outputPath) {
|
|
1377
|
-
return [
|
|
1378
|
-
"-y",
|
|
1379
|
-
"-i",
|
|
1380
|
-
inputPath,
|
|
1381
|
-
"-vf",
|
|
1382
|
-
`scale=-2:${SET_VIDEO_RENDITION.height}`,
|
|
1383
|
-
"-c:v",
|
|
1384
|
-
"libx264",
|
|
1385
|
-
"-preset",
|
|
1386
|
-
"medium",
|
|
1387
|
-
"-crf",
|
|
1388
|
-
String(SET_VIDEO_RENDITION.crf),
|
|
1389
|
-
"-force_key_frames",
|
|
1390
|
-
`expr:gte(t,n_forced*${SET_VIDEO_RENDITION.gopSeconds})`,
|
|
1391
|
-
"-pix_fmt",
|
|
1392
|
-
"yuv420p",
|
|
1393
|
-
"-c:a",
|
|
1394
|
-
"aac",
|
|
1395
|
-
"-b:a",
|
|
1396
|
-
SET_VIDEO_RENDITION.audioBitrate,
|
|
1397
|
-
"-movflags",
|
|
1398
|
-
"+faststart",
|
|
1399
|
-
outputPath
|
|
1400
|
-
];
|
|
1401
|
-
}
|
|
1402
|
-
async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
|
|
1403
|
-
if (!existsSync(masterPath)) {
|
|
1404
|
-
throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
|
|
1405
|
-
}
|
|
1406
|
-
await assertFfmpeg();
|
|
1407
|
-
const renditionPath = join4(tmpdir(), `fluncle-set-${randomUUID()}.mp4`);
|
|
1408
|
-
onProgress("Set video: deriving the 1080p faststart rendition (ffmpeg)…");
|
|
1409
|
-
await deriveRendition(masterPath, renditionPath);
|
|
1410
|
-
try {
|
|
1411
|
-
const size = statSync2(renditionPath).size;
|
|
1412
|
-
const plan = planMultipart(size);
|
|
1413
|
-
onProgress(`Set video: uploading ${(size / 1e9).toFixed(2)} GB in ${plan.partCount} part(s)…`);
|
|
1414
|
-
const presign = await adminApiPost(presignPath, {
|
|
1415
|
-
partCount: plan.partCount
|
|
1416
|
-
});
|
|
1417
|
-
const urlByPart = new Map(presign.parts.map((part) => [part.partNumber, part.url]));
|
|
1418
|
-
const completed = [];
|
|
1419
|
-
try {
|
|
1420
|
-
for (const part of plan.parts) {
|
|
1421
|
-
const url = urlByPart.get(part.partNumber);
|
|
1422
|
-
if (!url) {
|
|
1423
|
-
throw new CliError2("presign_missing", `Worker did not sign part ${part.partNumber} of ${plan.partCount}`);
|
|
1424
|
-
}
|
|
1425
|
-
onProgress(`Set video: part ${part.partNumber}/${plan.partCount}`);
|
|
1426
|
-
const etag = await putPart(url, renditionPath, part);
|
|
1427
|
-
completed.push({ etag, partNumber: part.partNumber });
|
|
1428
|
-
}
|
|
1429
|
-
await completeUpload(presign.completeUrl, completed);
|
|
1430
|
-
} catch (error) {
|
|
1431
|
-
await abortUpload(presign.abortUrl).catch(() => {});
|
|
1432
|
-
throw error;
|
|
1433
|
-
}
|
|
1434
|
-
return { key: presign.key, url: `${FOUND_BASE}/${presign.key}` };
|
|
1435
|
-
} finally {
|
|
1436
|
-
rmSync2(renditionPath, { force: true });
|
|
1437
|
-
}
|
|
1438
|
-
}
|
|
1439
|
-
async function stageSetVideo(mixtapeId, masterPath, onProgress = () => {}) {
|
|
1440
|
-
const result = await uploadRenditionMultipart(masterPath, `/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/set-video/presign`, onProgress);
|
|
1441
|
-
await adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}`, {
|
|
1442
|
-
setVideoAt: new Date().toISOString()
|
|
1443
|
-
});
|
|
1444
|
-
onProgress("Set video: flipped setVideoAt — the /log player + video SEO are live.");
|
|
1445
|
-
return result;
|
|
1446
|
-
}
|
|
1447
|
-
async function putPart(url, path2, part) {
|
|
1448
|
-
const response = await fetch(url, {
|
|
1449
|
-
body: Bun.file(path2).slice(part.start, part.end),
|
|
1450
|
-
method: "PUT"
|
|
1451
|
-
});
|
|
1452
|
-
if (!response.ok) {
|
|
1453
|
-
const detail = (await response.text().catch(() => "")).slice(0, 300);
|
|
1454
|
-
throw new CliError2("r2_part_failed", `R2 rejected part ${part.partNumber} (${response.status} ${response.statusText})${detail ? `: ${detail}` : ""}`);
|
|
1455
|
-
}
|
|
1456
|
-
const etag = response.headers.get("etag");
|
|
1457
|
-
if (!etag) {
|
|
1458
|
-
throw new CliError2("r2_no_etag", `R2 returned no ETag for part ${part.partNumber}`);
|
|
1459
|
-
}
|
|
1460
|
-
return etag;
|
|
1461
|
-
}
|
|
1462
|
-
async function completeUpload(url, parts) {
|
|
1463
|
-
const response = await fetch(url, {
|
|
1464
|
-
body: buildCompleteXml(parts),
|
|
1465
|
-
headers: { "content-type": "application/xml" },
|
|
1466
|
-
method: "POST"
|
|
1467
|
-
});
|
|
1468
|
-
const text = await response.text().catch(() => "");
|
|
1469
|
-
if (!response.ok || text.includes("<Error>")) {
|
|
1470
|
-
throw new CliError2("r2_complete_failed", `R2 CompleteMultipartUpload failed (${response.status} ${response.statusText})${text ? `: ${text.slice(0, 300)}` : ""}`);
|
|
1471
|
-
}
|
|
1472
|
-
}
|
|
1473
|
-
async function abortUpload(url) {
|
|
1474
|
-
await fetch(url, { method: "DELETE" });
|
|
1475
|
-
}
|
|
1476
|
-
async function assertFfmpeg() {
|
|
1477
|
-
try {
|
|
1478
|
-
const proc = Bun.spawn(["ffmpeg", "-version"], { stderr: "ignore", stdout: "ignore" });
|
|
1479
|
-
await proc.exited;
|
|
1480
|
-
if (proc.exitCode !== 0) {
|
|
1481
|
-
throw new Error("ffmpeg -version exited non-zero");
|
|
1482
|
-
}
|
|
1483
|
-
} catch {
|
|
1484
|
-
throw new CliError2("ffmpeg_missing", "--set-video needs ffmpeg to derive the 1080p rendition. Install it (brew install ffmpeg).");
|
|
1485
|
-
}
|
|
1486
|
-
}
|
|
1487
|
-
async function deriveRendition(inputPath, outputPath) {
|
|
1488
|
-
const proc = Bun.spawn(["ffmpeg", ...renditionFfmpegArgs(inputPath, outputPath)], {
|
|
1489
|
-
stderr: "pipe",
|
|
1490
|
-
stdout: "ignore"
|
|
1491
|
-
});
|
|
1492
|
-
await proc.exited;
|
|
1493
|
-
if (proc.exitCode !== 0) {
|
|
1494
|
-
const detail = (await new Response(proc.stderr).text().catch(() => "")).slice(-400);
|
|
1495
|
-
throw new CliError2("ffmpeg_failed", `ffmpeg failed to derive the set-video rendition${detail ? `: ${detail}` : ""}`);
|
|
1496
|
-
}
|
|
1497
|
-
}
|
|
1498
|
-
var FOUND_BASE = "https://found.fluncle.com", SET_VIDEO_RENDITION, DEFAULT_PART_SIZE, MIN_PART_SIZE, MAX_PARTS = 1e4;
|
|
1499
|
-
var init_mixtape_set_video = __esm(() => {
|
|
1500
|
-
init_api();
|
|
1501
|
-
init_output();
|
|
1502
|
-
SET_VIDEO_RENDITION = {
|
|
1503
|
-
audioBitrate: "192k",
|
|
1504
|
-
crf: 20,
|
|
1505
|
-
gopSeconds: 2,
|
|
1506
|
-
height: 1080
|
|
1507
|
-
};
|
|
1508
|
-
DEFAULT_PART_SIZE = 100 * 1024 * 1024;
|
|
1509
|
-
MIN_PART_SIZE = 5 * 1024 * 1024;
|
|
1510
|
-
});
|
|
1511
|
-
|
|
1512
1332
|
// src/commands/mixtapes.ts
|
|
1513
1333
|
var exports_mixtapes = {};
|
|
1514
1334
|
__export(exports_mixtapes, {
|
|
@@ -1523,7 +1343,7 @@ __export(exports_mixtapes, {
|
|
|
1523
1343
|
mixtapeDeleteCommand: () => mixtapeDeleteCommand,
|
|
1524
1344
|
mixtapeCreateCommand: () => mixtapeCreateCommand
|
|
1525
1345
|
});
|
|
1526
|
-
import { existsSync
|
|
1346
|
+
import { existsSync, readFileSync as readFileSync2 } from "node:fs";
|
|
1527
1347
|
async function mixtapesCommand() {
|
|
1528
1348
|
const response = await publicApiGet("/api/mixtapes");
|
|
1529
1349
|
return response.mixtapes;
|
|
@@ -1552,21 +1372,24 @@ async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {
|
|
|
1552
1372
|
if (!mixtape.id) {
|
|
1553
1373
|
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
1554
1374
|
}
|
|
1555
|
-
|
|
1375
|
+
if (mixtape.status === "draft") {
|
|
1376
|
+
throw new CliError2("mixtape_not_promoted", `${mixtape.id} is still a draft — promote its recording first:
|
|
1377
|
+
` + " fluncle admin recordings promote <recordingId>");
|
|
1378
|
+
}
|
|
1379
|
+
const both = !options.youtube && !options.mixcloud;
|
|
1556
1380
|
const doYoutube = both || Boolean(options.youtube);
|
|
1557
1381
|
const doMixcloud = both || Boolean(options.mixcloud);
|
|
1558
|
-
const doSetVideo = Boolean(options.setVideo);
|
|
1559
1382
|
if (doYoutube && !options.video) {
|
|
1560
1383
|
throw new CliError2("missing_video", "YouTube distribution needs --video <mp4>");
|
|
1561
1384
|
}
|
|
1562
1385
|
if (doMixcloud && !options.audio) {
|
|
1563
1386
|
throw new CliError2("missing_audio", "Mixcloud distribution needs --audio <file>");
|
|
1564
1387
|
}
|
|
1565
|
-
if (doSetVideo && !options.video) {
|
|
1566
|
-
throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
|
|
1567
|
-
}
|
|
1568
1388
|
const mixtapeId = mixtape.id;
|
|
1569
|
-
|
|
1389
|
+
const logId = mixtape.logId;
|
|
1390
|
+
if (!logId) {
|
|
1391
|
+
throw new CliError2("missing_log_id", "The mixtape has no Log ID — was it promoted successfully?");
|
|
1392
|
+
}
|
|
1570
1393
|
if (!mixtape.durationMs) {
|
|
1571
1394
|
const source = options.audio ?? options.video;
|
|
1572
1395
|
const durationMs = source ? await probeDurationMs(source) : undefined;
|
|
@@ -1575,18 +1398,10 @@ async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {
|
|
|
1575
1398
|
onProgress(`Duration: ${Math.round(durationMs / 60000)} min (from the upload).`);
|
|
1576
1399
|
}
|
|
1577
1400
|
}
|
|
1578
|
-
if (mixtape.status === "
|
|
1579
|
-
onProgress("Minting the coordinate…");
|
|
1580
|
-
const published = await mixtapePublishCommand(mixtapeId);
|
|
1581
|
-
logId = published.mixtape.logId;
|
|
1582
|
-
onProgress(`Minted ${logId}.`);
|
|
1583
|
-
} else if (mixtape.status === "published") {
|
|
1401
|
+
if (mixtape.status === "published") {
|
|
1584
1402
|
onProgress(`Already published (${logId}); re-distributing.`);
|
|
1585
1403
|
} else {
|
|
1586
|
-
onProgress(`
|
|
1587
|
-
}
|
|
1588
|
-
if (!logId) {
|
|
1589
|
-
throw new CliError2("mint_failed", "The mixtape has no Log ID after minting");
|
|
1404
|
+
onProgress(`Distributing ${logId} (promoted — coordinate already minted).`);
|
|
1590
1405
|
}
|
|
1591
1406
|
const results = [];
|
|
1592
1407
|
if (doYoutube) {
|
|
@@ -1609,16 +1424,6 @@ async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {
|
|
|
1609
1424
|
results.push({ platform: "mixcloud", url: result.url });
|
|
1610
1425
|
onProgress(`Mixcloud: ${result.url}`);
|
|
1611
1426
|
}
|
|
1612
|
-
if (doSetVideo) {
|
|
1613
|
-
if (!options.video) {
|
|
1614
|
-
throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
|
|
1615
|
-
}
|
|
1616
|
-
onProgress("Set video: staging the 1080p rendition…");
|
|
1617
|
-
const { stageSetVideo: stageSetVideo2 } = await Promise.resolve().then(() => (init_mixtape_set_video(), exports_mixtape_set_video));
|
|
1618
|
-
const result = await stageSetVideo2(mixtapeId, options.video, onProgress);
|
|
1619
|
-
results.push({ platform: "set-video", url: result.url });
|
|
1620
|
-
onProgress(`Set video: ${result.url}`);
|
|
1621
|
-
}
|
|
1622
1427
|
return { logId, mixtapeId, results };
|
|
1623
1428
|
}
|
|
1624
1429
|
async function mixtapeResyncCommand(idOrLogId, options, onProgress = () => {}) {
|
|
@@ -1691,7 +1496,7 @@ function buildBody(options) {
|
|
|
1691
1496
|
return body;
|
|
1692
1497
|
}
|
|
1693
1498
|
function parseCueFile(filePath) {
|
|
1694
|
-
if (!
|
|
1499
|
+
if (!existsSync(filePath)) {
|
|
1695
1500
|
throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
|
|
1696
1501
|
}
|
|
1697
1502
|
const text = readFileSync2(filePath, "utf-8");
|
|
@@ -2446,7 +2251,7 @@ function parseVersion2(version) {
|
|
|
2446
2251
|
var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
|
|
2447
2252
|
var init_version2 = __esm(() => {
|
|
2448
2253
|
init_output();
|
|
2449
|
-
currentVersion2 = "0.
|
|
2254
|
+
currentVersion2 = "0.87.0".trim() ? "0.87.0".trim() : "0.1.0";
|
|
2450
2255
|
});
|
|
2451
2256
|
|
|
2452
2257
|
// ../../packages/registry/src/index.ts
|
|
@@ -3200,7 +3005,7 @@ async function trackVideoCommand(idOrLogId, files, onProgress) {
|
|
|
3200
3005
|
const detail = (await response.text().catch(() => "")).slice(0, 300);
|
|
3201
3006
|
throw new CliError2("r2_put_failed", `R2 rejected ${spec.field} with ${response.status} ${response.statusText}${detail ? `: ${detail}` : ""}`);
|
|
3202
3007
|
}
|
|
3203
|
-
urls[spec.field] = `${
|
|
3008
|
+
urls[spec.field] = `${FOUND_BASE}/${upload.key}`;
|
|
3204
3009
|
}
|
|
3205
3010
|
const manifest = files.render ? await readManifestFields(files.render) : {};
|
|
3206
3011
|
const videoModel = files.model?.trim().slice(0, 120) || manifest.model || DEFAULT_VIDEO_MODEL;
|
|
@@ -3311,7 +3116,7 @@ async function trackNoteCommand(idOrLogId, options) {
|
|
|
3311
3116
|
const body = { note: options.note };
|
|
3312
3117
|
return adminApiPost(`/api/admin/tracks/${encodeURIComponent(idOrLogId)}/note`, body);
|
|
3313
3118
|
}
|
|
3314
|
-
var DEFAULT_VIDEO_MODEL = "anthropic/claude-opus-4-8", DEFAULT_VIDEO_REASONING = "high",
|
|
3119
|
+
var DEFAULT_VIDEO_MODEL = "anthropic/claude-opus-4-8", DEFAULT_VIDEO_REASONING = "high", FOUND_BASE = "https://found.fluncle.com", VIDEO_FIELDS;
|
|
3315
3120
|
var init_track = __esm(() => {
|
|
3316
3121
|
init_api();
|
|
3317
3122
|
init_output();
|
|
@@ -3705,7 +3510,7 @@ __export(exports_mixtape_youtube2, {
|
|
|
3705
3510
|
distributeYoutube: () => distributeYoutube2,
|
|
3706
3511
|
authYoutubeCommand: () => authYoutubeCommand2
|
|
3707
3512
|
});
|
|
3708
|
-
import { statSync as
|
|
3513
|
+
import { statSync as statSync2 } from "node:fs";
|
|
3709
3514
|
async function distributeYoutube2(mixtapeId, videoPath, onProgress) {
|
|
3710
3515
|
const contentLength = fileSize2(videoPath);
|
|
3711
3516
|
const contentType = "video/mp4";
|
|
@@ -3841,7 +3646,7 @@ async function resolveMixtapeId2(idOrLogId) {
|
|
|
3841
3646
|
}
|
|
3842
3647
|
function fileSize2(path2) {
|
|
3843
3648
|
try {
|
|
3844
|
-
return
|
|
3649
|
+
return statSync2(path2).size;
|
|
3845
3650
|
} catch {
|
|
3846
3651
|
throw new CliError2("video_not_found", `Cannot read video file at ${path2}`);
|
|
3847
3652
|
}
|
|
@@ -3861,75 +3666,235 @@ var init_mixtape_youtube2 = __esm(() => {
|
|
|
3861
3666
|
init_output();
|
|
3862
3667
|
});
|
|
3863
3668
|
|
|
3864
|
-
// src/commands/
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
recordingGet: () => recordingGet,
|
|
3873
|
-
recordingDeleteCommand: () => recordingDeleteCommand,
|
|
3874
|
-
recordingCreateCommand: () => recordingCreateCommand
|
|
3875
|
-
});
|
|
3876
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
3877
|
-
async function recordingGet(id) {
|
|
3878
|
-
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
3879
|
-
return response.recording;
|
|
3880
|
-
}
|
|
3881
|
-
function formatRecordingSummary(recording) {
|
|
3882
|
-
const kind = recording.hasVideo ? `take v${recording.version}` : "plan";
|
|
3883
|
-
const promoted = recording.logId ? ` → fluncle://${recording.logId}` : " (un-promoted)";
|
|
3884
|
-
const cues = recording.tracklist.length;
|
|
3885
|
-
return `${recording.id} [${kind}] ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
|
|
3886
|
-
}
|
|
3887
|
-
async function recordingsListCommand(options = {}) {
|
|
3888
|
-
const query = new URLSearchParams;
|
|
3889
|
-
if (options.kind) {
|
|
3890
|
-
query.set("kind", options.kind);
|
|
3891
|
-
}
|
|
3892
|
-
if (options.parentId) {
|
|
3893
|
-
query.set("parentId", options.parentId);
|
|
3669
|
+
// src/commands/mixtape-set-video.ts
|
|
3670
|
+
import { randomUUID } from "node:crypto";
|
|
3671
|
+
import { existsSync as existsSync2, rmSync as rmSync2, statSync as statSync3 } from "node:fs";
|
|
3672
|
+
import { tmpdir } from "node:os";
|
|
3673
|
+
import { join as join4 } from "node:path";
|
|
3674
|
+
function planMultipart(contentLength, partSize = DEFAULT_PART_SIZE) {
|
|
3675
|
+
if (!Number.isInteger(contentLength) || contentLength <= 0) {
|
|
3676
|
+
throw new CliError2("invalid_size", `The rendition size must be a positive integer (got ${contentLength})`);
|
|
3894
3677
|
}
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
printJson2({ ok: true, recordings: response.recordings });
|
|
3899
|
-
return;
|
|
3678
|
+
let effective = Math.max(partSize, MIN_PART_SIZE);
|
|
3679
|
+
if (Math.ceil(contentLength / effective) > MAX_PARTS) {
|
|
3680
|
+
effective = Math.ceil(contentLength / MAX_PARTS);
|
|
3900
3681
|
}
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3682
|
+
const parts = [];
|
|
3683
|
+
let start = 0;
|
|
3684
|
+
let partNumber = 1;
|
|
3685
|
+
while (start < contentLength) {
|
|
3686
|
+
const end = Math.min(start + effective, contentLength);
|
|
3687
|
+
parts.push({ end, partNumber, size: end - start, start });
|
|
3688
|
+
start = end;
|
|
3689
|
+
partNumber += 1;
|
|
3904
3690
|
}
|
|
3905
|
-
|
|
3906
|
-
`));
|
|
3691
|
+
return { partCount: parts.length, partSize: effective, parts };
|
|
3907
3692
|
}
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
throw new CliError2("missing_id", "Missing recording id for: get");
|
|
3911
|
-
}
|
|
3912
|
-
const recording = await recordingGet(id);
|
|
3913
|
-
if (options.json) {
|
|
3914
|
-
printJson2({ ok: true, recording });
|
|
3915
|
-
return;
|
|
3916
|
-
}
|
|
3917
|
-
console.log(formatRecordingSummary(recording));
|
|
3918
|
-
console.log(` r2Key: ${recording.r2Key ?? "— (no set video)"}`);
|
|
3919
|
-
for (const cue of recording.tracklist) {
|
|
3920
|
-
const at = cue.startMs === undefined ? "—" : `${(cue.startMs / 1000).toFixed(1)}s`;
|
|
3921
|
-
console.log(` ${at} ${cue.artists.join(", ")} — ${cue.title}`);
|
|
3922
|
-
}
|
|
3693
|
+
function escapeXml(value) {
|
|
3694
|
+
return value.replace(/[<>&'"]/g, (char) => ({ '"': """, "&": "&", "'": "'", "<": "<", ">": ">" })[char] ?? char);
|
|
3923
3695
|
}
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3696
|
+
function buildCompleteXml(parts) {
|
|
3697
|
+
const ordered = [...parts].sort((a, b) => a.partNumber - b.partNumber);
|
|
3698
|
+
const body = ordered.map((part) => `<Part><PartNumber>${part.partNumber}</PartNumber><ETag>${escapeXml(part.etag)}</ETag></Part>`).join("");
|
|
3699
|
+
return `<CompleteMultipartUpload>${body}</CompleteMultipartUpload>`;
|
|
3700
|
+
}
|
|
3701
|
+
function renditionFfmpegArgs(inputPath, outputPath) {
|
|
3702
|
+
return [
|
|
3703
|
+
"-y",
|
|
3704
|
+
"-i",
|
|
3705
|
+
inputPath,
|
|
3706
|
+
"-vf",
|
|
3707
|
+
`scale=-2:${SET_VIDEO_RENDITION.height}`,
|
|
3708
|
+
"-c:v",
|
|
3709
|
+
"libx264",
|
|
3710
|
+
"-preset",
|
|
3711
|
+
"medium",
|
|
3712
|
+
"-crf",
|
|
3713
|
+
String(SET_VIDEO_RENDITION.crf),
|
|
3714
|
+
"-force_key_frames",
|
|
3715
|
+
`expr:gte(t,n_forced*${SET_VIDEO_RENDITION.gopSeconds})`,
|
|
3716
|
+
"-pix_fmt",
|
|
3717
|
+
"yuv420p",
|
|
3718
|
+
"-c:a",
|
|
3719
|
+
"aac",
|
|
3720
|
+
"-b:a",
|
|
3721
|
+
SET_VIDEO_RENDITION.audioBitrate,
|
|
3722
|
+
"-movflags",
|
|
3723
|
+
"+faststart",
|
|
3724
|
+
outputPath
|
|
3725
|
+
];
|
|
3726
|
+
}
|
|
3727
|
+
async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
|
|
3728
|
+
if (!existsSync2(masterPath)) {
|
|
3729
|
+
throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
|
|
3730
|
+
}
|
|
3731
|
+
await assertFfmpeg();
|
|
3732
|
+
const renditionPath = join4(tmpdir(), `fluncle-set-${randomUUID()}.mp4`);
|
|
3733
|
+
onProgress("Set video: deriving the 1080p faststart rendition (ffmpeg)…");
|
|
3734
|
+
await deriveRendition(masterPath, renditionPath);
|
|
3735
|
+
try {
|
|
3736
|
+
const size = statSync3(renditionPath).size;
|
|
3737
|
+
const plan = planMultipart(size);
|
|
3738
|
+
onProgress(`Set video: uploading ${(size / 1e9).toFixed(2)} GB in ${plan.partCount} part(s)…`);
|
|
3739
|
+
const presign = await adminApiPost(presignPath, {
|
|
3740
|
+
partCount: plan.partCount
|
|
3741
|
+
});
|
|
3742
|
+
const urlByPart = new Map(presign.parts.map((part) => [part.partNumber, part.url]));
|
|
3743
|
+
const completed = [];
|
|
3744
|
+
try {
|
|
3745
|
+
for (const part of plan.parts) {
|
|
3746
|
+
const url = urlByPart.get(part.partNumber);
|
|
3747
|
+
if (!url) {
|
|
3748
|
+
throw new CliError2("presign_missing", `Worker did not sign part ${part.partNumber} of ${plan.partCount}`);
|
|
3749
|
+
}
|
|
3750
|
+
onProgress(`Set video: part ${part.partNumber}/${plan.partCount}`);
|
|
3751
|
+
const etag = await putPart(url, renditionPath, part);
|
|
3752
|
+
completed.push({ etag, partNumber: part.partNumber });
|
|
3753
|
+
}
|
|
3754
|
+
await completeUpload(presign.completeUrl, completed);
|
|
3755
|
+
} catch (error) {
|
|
3756
|
+
await abortUpload(presign.abortUrl).catch(() => {});
|
|
3757
|
+
throw error;
|
|
3758
|
+
}
|
|
3759
|
+
return { key: presign.key, url: `${FOUND_BASE2}/${presign.key}` };
|
|
3760
|
+
} finally {
|
|
3761
|
+
rmSync2(renditionPath, { force: true });
|
|
3762
|
+
}
|
|
3763
|
+
}
|
|
3764
|
+
async function putPart(url, path2, part) {
|
|
3765
|
+
const response = await fetch(url, {
|
|
3766
|
+
body: Bun.file(path2).slice(part.start, part.end),
|
|
3767
|
+
method: "PUT"
|
|
3768
|
+
});
|
|
3769
|
+
if (!response.ok) {
|
|
3770
|
+
const detail = (await response.text().catch(() => "")).slice(0, 300);
|
|
3771
|
+
throw new CliError2("r2_part_failed", `R2 rejected part ${part.partNumber} (${response.status} ${response.statusText})${detail ? `: ${detail}` : ""}`);
|
|
3772
|
+
}
|
|
3773
|
+
const etag = response.headers.get("etag");
|
|
3774
|
+
if (!etag) {
|
|
3775
|
+
throw new CliError2("r2_no_etag", `R2 returned no ETag for part ${part.partNumber}`);
|
|
3776
|
+
}
|
|
3777
|
+
return etag;
|
|
3778
|
+
}
|
|
3779
|
+
async function completeUpload(url, parts) {
|
|
3780
|
+
const response = await fetch(url, {
|
|
3781
|
+
body: buildCompleteXml(parts),
|
|
3782
|
+
headers: { "content-type": "application/xml" },
|
|
3783
|
+
method: "POST"
|
|
3784
|
+
});
|
|
3785
|
+
const text = await response.text().catch(() => "");
|
|
3786
|
+
if (!response.ok || text.includes("<Error>")) {
|
|
3787
|
+
throw new CliError2("r2_complete_failed", `R2 CompleteMultipartUpload failed (${response.status} ${response.statusText})${text ? `: ${text.slice(0, 300)}` : ""}`);
|
|
3788
|
+
}
|
|
3789
|
+
}
|
|
3790
|
+
async function abortUpload(url) {
|
|
3791
|
+
await fetch(url, { method: "DELETE" });
|
|
3792
|
+
}
|
|
3793
|
+
async function assertFfmpeg() {
|
|
3794
|
+
try {
|
|
3795
|
+
const proc = Bun.spawn(["ffmpeg", "-version"], { stderr: "ignore", stdout: "ignore" });
|
|
3796
|
+
await proc.exited;
|
|
3797
|
+
if (proc.exitCode !== 0) {
|
|
3798
|
+
throw new Error("ffmpeg -version exited non-zero");
|
|
3799
|
+
}
|
|
3800
|
+
} catch {
|
|
3801
|
+
throw new CliError2("ffmpeg_missing", "--set-video needs ffmpeg to derive the 1080p rendition. Install it (brew install ffmpeg).");
|
|
3802
|
+
}
|
|
3803
|
+
}
|
|
3804
|
+
async function deriveRendition(inputPath, outputPath) {
|
|
3805
|
+
const proc = Bun.spawn(["ffmpeg", ...renditionFfmpegArgs(inputPath, outputPath)], {
|
|
3806
|
+
stderr: "pipe",
|
|
3807
|
+
stdout: "ignore"
|
|
3808
|
+
});
|
|
3809
|
+
await proc.exited;
|
|
3810
|
+
if (proc.exitCode !== 0) {
|
|
3811
|
+
const detail = (await new Response(proc.stderr).text().catch(() => "")).slice(-400);
|
|
3812
|
+
throw new CliError2("ffmpeg_failed", `ffmpeg failed to derive the set-video rendition${detail ? `: ${detail}` : ""}`);
|
|
3813
|
+
}
|
|
3814
|
+
}
|
|
3815
|
+
var FOUND_BASE2 = "https://found.fluncle.com", SET_VIDEO_RENDITION, DEFAULT_PART_SIZE, MIN_PART_SIZE, MAX_PARTS = 1e4;
|
|
3816
|
+
var init_mixtape_set_video = __esm(() => {
|
|
3817
|
+
init_api();
|
|
3818
|
+
init_output();
|
|
3819
|
+
SET_VIDEO_RENDITION = {
|
|
3820
|
+
audioBitrate: "192k",
|
|
3821
|
+
crf: 20,
|
|
3822
|
+
gopSeconds: 2,
|
|
3823
|
+
height: 1080
|
|
3824
|
+
};
|
|
3825
|
+
DEFAULT_PART_SIZE = 100 * 1024 * 1024;
|
|
3826
|
+
MIN_PART_SIZE = 5 * 1024 * 1024;
|
|
3827
|
+
});
|
|
3828
|
+
|
|
3829
|
+
// src/commands/recordings.ts
|
|
3830
|
+
var exports_recordings = {};
|
|
3831
|
+
__export(exports_recordings, {
|
|
3832
|
+
recordingsListCommand: () => recordingsListCommand,
|
|
3833
|
+
recordingUpdateCommand: () => recordingUpdateCommand,
|
|
3834
|
+
recordingReplaceCuesCommand: () => recordingReplaceCuesCommand,
|
|
3835
|
+
recordingPromoteCommand: () => recordingPromoteCommand,
|
|
3836
|
+
recordingGetCommand: () => recordingGetCommand,
|
|
3837
|
+
recordingGet: () => recordingGet,
|
|
3838
|
+
recordingDeleteCommand: () => recordingDeleteCommand,
|
|
3839
|
+
recordingCreateCommand: () => recordingCreateCommand
|
|
3840
|
+
});
|
|
3841
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
3842
|
+
async function recordingGet(id) {
|
|
3843
|
+
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
3844
|
+
return response.recording;
|
|
3845
|
+
}
|
|
3846
|
+
function formatRecordingSummary(recording) {
|
|
3847
|
+
const kind = recording.hasVideo ? `take v${recording.version}` : "plan";
|
|
3848
|
+
const promoted = recording.logId ? ` → fluncle://${recording.logId}` : " (un-promoted)";
|
|
3849
|
+
const cues = recording.tracklist.length;
|
|
3850
|
+
return `${recording.id} [${kind}] ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
|
|
3851
|
+
}
|
|
3852
|
+
async function recordingsListCommand(options = {}) {
|
|
3853
|
+
const query = new URLSearchParams;
|
|
3854
|
+
if (options.kind) {
|
|
3855
|
+
query.set("kind", options.kind);
|
|
3856
|
+
}
|
|
3857
|
+
if (options.parentId) {
|
|
3858
|
+
query.set("parentId", options.parentId);
|
|
3859
|
+
}
|
|
3860
|
+
const suffix = query.toString() ? `?${query.toString()}` : "";
|
|
3861
|
+
const response = await adminApiGet(`/api/admin/recordings${suffix}`);
|
|
3862
|
+
if (options.json) {
|
|
3863
|
+
printJson2({ ok: true, recordings: response.recordings });
|
|
3864
|
+
return;
|
|
3865
|
+
}
|
|
3866
|
+
if (response.recordings.length === 0) {
|
|
3867
|
+
console.log("No recordings yet.");
|
|
3868
|
+
return;
|
|
3869
|
+
}
|
|
3870
|
+
console.log(response.recordings.map(formatRecordingSummary).join(`
|
|
3871
|
+
`));
|
|
3872
|
+
}
|
|
3873
|
+
async function recordingGetCommand(id, options = {}) {
|
|
3874
|
+
if (!id) {
|
|
3875
|
+
throw new CliError2("missing_id", "Missing recording id for: get");
|
|
3876
|
+
}
|
|
3877
|
+
const recording = await recordingGet(id);
|
|
3878
|
+
if (options.json) {
|
|
3879
|
+
printJson2({ ok: true, recording });
|
|
3880
|
+
return;
|
|
3881
|
+
}
|
|
3882
|
+
console.log(formatRecordingSummary(recording));
|
|
3883
|
+
console.log(` r2Key: ${recording.r2Key ?? "— (no set video)"}`);
|
|
3884
|
+
for (const cue of recording.tracklist) {
|
|
3885
|
+
const at = cue.startMs === undefined ? "—" : `${(cue.startMs / 1000).toFixed(1)}s`;
|
|
3886
|
+
console.log(` ${at} ${cue.artists.join(", ")} — ${cue.title}`);
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
async function recordingCreateCommand(options = {}) {
|
|
3890
|
+
if (options.plan) {
|
|
3891
|
+
const created2 = await adminApiPost("/api/admin/recordings", {
|
|
3892
|
+
kind: "plan",
|
|
3893
|
+
recordedAt: options.recordedAt
|
|
3894
|
+
});
|
|
3895
|
+
const recording2 = created2.recording;
|
|
3896
|
+
if (options.json) {
|
|
3897
|
+
printJson2({ ok: true, recording: recording2 });
|
|
3933
3898
|
return;
|
|
3934
3899
|
}
|
|
3935
3900
|
console.log(`Plan ${recording2.id} created — handle "${recording2.title}"`);
|
|
@@ -4049,253 +4014,6 @@ var init_recordings = __esm(() => {
|
|
|
4049
4014
|
init_mixtape_set_video();
|
|
4050
4015
|
});
|
|
4051
4016
|
|
|
4052
|
-
// src/commands/mixtapes.ts
|
|
4053
|
-
var exports_mixtapes2 = {};
|
|
4054
|
-
__export(exports_mixtapes2, {
|
|
4055
|
-
mixtapesCommand: () => mixtapesCommand2,
|
|
4056
|
-
mixtapeUpdateCommand: () => mixtapeUpdateCommand2,
|
|
4057
|
-
mixtapeResyncCommand: () => mixtapeResyncCommand2,
|
|
4058
|
-
mixtapePublishCommand: () => mixtapePublishCommand2,
|
|
4059
|
-
mixtapeMembersCommand: () => mixtapeMembersCommand2,
|
|
4060
|
-
mixtapeListCommand: () => mixtapeListCommand,
|
|
4061
|
-
mixtapeGetCommand: () => mixtapeGetCommand,
|
|
4062
|
-
mixtapeDistributeCommand: () => mixtapeDistributeCommand2,
|
|
4063
|
-
mixtapeDeleteCommand: () => mixtapeDeleteCommand2,
|
|
4064
|
-
mixtapeCreateCommand: () => mixtapeCreateCommand2
|
|
4065
|
-
});
|
|
4066
|
-
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
4067
|
-
async function mixtapesCommand2() {
|
|
4068
|
-
const response = await publicApiGet("/api/mixtapes");
|
|
4069
|
-
return response.mixtapes;
|
|
4070
|
-
}
|
|
4071
|
-
async function mixtapeCreateCommand2(options) {
|
|
4072
|
-
return adminApiPost("/api/admin/mixtapes", buildBody2(options));
|
|
4073
|
-
}
|
|
4074
|
-
async function mixtapeUpdateCommand2(id, options) {
|
|
4075
|
-
return adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(id)}`, buildBody2(options));
|
|
4076
|
-
}
|
|
4077
|
-
async function mixtapeMembersCommand2(id, refs, options) {
|
|
4078
|
-
const members = refs.map((ref) => ({ ref }));
|
|
4079
|
-
if (options.from) {
|
|
4080
|
-
members.push(...parseCueFile2(options.from));
|
|
4081
|
-
}
|
|
4082
|
-
return adminApiPut(`/api/admin/mixtapes/${encodeURIComponent(id)}/members`, { members });
|
|
4083
|
-
}
|
|
4084
|
-
async function mixtapePublishCommand2(id) {
|
|
4085
|
-
return adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(id)}/publish`);
|
|
4086
|
-
}
|
|
4087
|
-
async function mixtapeDeleteCommand2(id) {
|
|
4088
|
-
return adminApiDelete(`/api/admin/mixtapes/${encodeURIComponent(id)}`);
|
|
4089
|
-
}
|
|
4090
|
-
async function mixtapeDistributeCommand2(idOrLogId, options, onProgress = () => {}) {
|
|
4091
|
-
const mixtape = await mixtapeGetCommand(idOrLogId);
|
|
4092
|
-
if (!mixtape.id) {
|
|
4093
|
-
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
4094
|
-
}
|
|
4095
|
-
const both = !options.youtube && !options.mixcloud && !options.setVideo;
|
|
4096
|
-
const doYoutube = both || Boolean(options.youtube);
|
|
4097
|
-
const doMixcloud = both || Boolean(options.mixcloud);
|
|
4098
|
-
const doSetVideo = Boolean(options.setVideo);
|
|
4099
|
-
if (doYoutube && !options.video) {
|
|
4100
|
-
throw new CliError2("missing_video", "YouTube distribution needs --video <mp4>");
|
|
4101
|
-
}
|
|
4102
|
-
if (doMixcloud && !options.audio) {
|
|
4103
|
-
throw new CliError2("missing_audio", "Mixcloud distribution needs --audio <file>");
|
|
4104
|
-
}
|
|
4105
|
-
if (doSetVideo && !options.video) {
|
|
4106
|
-
throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
|
|
4107
|
-
}
|
|
4108
|
-
const mixtapeId = mixtape.id;
|
|
4109
|
-
let logId = mixtape.logId;
|
|
4110
|
-
if (!mixtape.durationMs) {
|
|
4111
|
-
const source = options.audio ?? options.video;
|
|
4112
|
-
const durationMs = source ? await probeDurationMs2(source) : undefined;
|
|
4113
|
-
if (durationMs) {
|
|
4114
|
-
await mixtapeUpdateCommand2(mixtapeId, { durationMs: String(durationMs), json: false });
|
|
4115
|
-
onProgress(`Duration: ${Math.round(durationMs / 60000)} min (from the upload).`);
|
|
4116
|
-
}
|
|
4117
|
-
}
|
|
4118
|
-
if (mixtape.status === "draft") {
|
|
4119
|
-
onProgress("Minting the coordinate…");
|
|
4120
|
-
const published = await mixtapePublishCommand2(mixtapeId);
|
|
4121
|
-
logId = published.mixtape.logId;
|
|
4122
|
-
onProgress(`Minted ${logId}.`);
|
|
4123
|
-
} else if (mixtape.status === "published") {
|
|
4124
|
-
onProgress(`Already published (${logId}); re-distributing.`);
|
|
4125
|
-
} else {
|
|
4126
|
-
onProgress(`Resuming distribution for ${logId}.`);
|
|
4127
|
-
}
|
|
4128
|
-
if (!logId) {
|
|
4129
|
-
throw new CliError2("mint_failed", "The mixtape has no Log ID after minting");
|
|
4130
|
-
}
|
|
4131
|
-
const results = [];
|
|
4132
|
-
if (doYoutube) {
|
|
4133
|
-
if (!options.video) {
|
|
4134
|
-
throw new CliError2("missing_video", "YouTube distribution needs --video <mp4>");
|
|
4135
|
-
}
|
|
4136
|
-
onProgress("YouTube: uploading video…");
|
|
4137
|
-
const { distributeYoutube: distributeYoutube3 } = await Promise.resolve().then(() => (init_mixtape_youtube(), exports_mixtape_youtube));
|
|
4138
|
-
const result = await distributeYoutube3(mixtapeId, options.video, onProgress);
|
|
4139
|
-
results.push({ platform: "youtube", url: result.url });
|
|
4140
|
-
onProgress(`YouTube: ${result.url}`);
|
|
4141
|
-
}
|
|
4142
|
-
if (doMixcloud) {
|
|
4143
|
-
if (!options.audio) {
|
|
4144
|
-
throw new CliError2("missing_audio", "Mixcloud distribution needs --audio <file>");
|
|
4145
|
-
}
|
|
4146
|
-
onProgress("Mixcloud: uploading audio…");
|
|
4147
|
-
const { distributeMixcloud: distributeMixcloud2 } = await Promise.resolve().then(() => (init_mixtape_mixcloud(), exports_mixtape_mixcloud));
|
|
4148
|
-
const result = await distributeMixcloud2(mixtapeId, options.audio, onProgress, options.unlisted);
|
|
4149
|
-
results.push({ platform: "mixcloud", url: result.url });
|
|
4150
|
-
onProgress(`Mixcloud: ${result.url}`);
|
|
4151
|
-
}
|
|
4152
|
-
if (doSetVideo) {
|
|
4153
|
-
if (!options.video) {
|
|
4154
|
-
throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
|
|
4155
|
-
}
|
|
4156
|
-
onProgress("Set video: staging the 1080p rendition…");
|
|
4157
|
-
const { stageSetVideo: stageSetVideo2 } = await Promise.resolve().then(() => (init_mixtape_set_video(), exports_mixtape_set_video));
|
|
4158
|
-
const result = await stageSetVideo2(mixtapeId, options.video, onProgress);
|
|
4159
|
-
results.push({ platform: "set-video", url: result.url });
|
|
4160
|
-
onProgress(`Set video: ${result.url}`);
|
|
4161
|
-
}
|
|
4162
|
-
return { logId, mixtapeId, results };
|
|
4163
|
-
}
|
|
4164
|
-
async function mixtapeResyncCommand2(idOrLogId, options, onProgress = () => {}) {
|
|
4165
|
-
const mixtape = await mixtapeGetCommand(idOrLogId);
|
|
4166
|
-
if (!mixtape.id) {
|
|
4167
|
-
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
4168
|
-
}
|
|
4169
|
-
if (!mixtape.logId) {
|
|
4170
|
-
throw new CliError2("mixtape_no_log_id", "The mixtape isn't published yet — distribute it before re-syncing.");
|
|
4171
|
-
}
|
|
4172
|
-
const mixtapeId = mixtape.id;
|
|
4173
|
-
const explicit = Boolean(options.youtube) || Boolean(options.mixcloud);
|
|
4174
|
-
let doYoutube = Boolean(options.youtube);
|
|
4175
|
-
let doMixcloud = Boolean(options.mixcloud);
|
|
4176
|
-
if (!explicit) {
|
|
4177
|
-
const social = await adminApiGet(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/social`);
|
|
4178
|
-
const platforms = new Set(social.posts.map((post) => post.platform));
|
|
4179
|
-
doYoutube = platforms.has("youtube");
|
|
4180
|
-
doMixcloud = platforms.has("mixcloud");
|
|
4181
|
-
if (!doYoutube && !doMixcloud) {
|
|
4182
|
-
throw new CliError2("mixtape_not_distributed", "The mixtape has no YouTube or Mixcloud link to re-sync.");
|
|
4183
|
-
}
|
|
4184
|
-
}
|
|
4185
|
-
const results = [];
|
|
4186
|
-
if (doYoutube) {
|
|
4187
|
-
onProgress("YouTube: re-syncing description + chapters…");
|
|
4188
|
-
const { resyncYoutube: resyncYoutube3 } = await Promise.resolve().then(() => (init_mixtape_youtube(), exports_mixtape_youtube));
|
|
4189
|
-
const result = await resyncYoutube3(mixtapeId);
|
|
4190
|
-
results.push({ platform: "youtube", url: result.url });
|
|
4191
|
-
onProgress(`YouTube: ${result.url}`);
|
|
4192
|
-
}
|
|
4193
|
-
if (doMixcloud) {
|
|
4194
|
-
onProgress("Mixcloud: re-syncing sections…");
|
|
4195
|
-
const { resyncMixcloud: resyncMixcloud2 } = await Promise.resolve().then(() => (init_mixtape_mixcloud(), exports_mixtape_mixcloud));
|
|
4196
|
-
const result = await resyncMixcloud2(mixtapeId);
|
|
4197
|
-
results.push({ platform: "mixcloud", url: result.url });
|
|
4198
|
-
onProgress(`Mixcloud: ${result.url}`);
|
|
4199
|
-
}
|
|
4200
|
-
return { logId: mixtape.logId, mixtapeId, results };
|
|
4201
|
-
}
|
|
4202
|
-
async function probeDurationMs2(filePath) {
|
|
4203
|
-
try {
|
|
4204
|
-
const proc = Bun.spawn(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", filePath], { stderr: "ignore", stdout: "pipe" });
|
|
4205
|
-
const out = await new Response(proc.stdout).text();
|
|
4206
|
-
await proc.exited;
|
|
4207
|
-
const seconds = Number.parseFloat(out.trim());
|
|
4208
|
-
return Number.isFinite(seconds) && seconds > 0 ? Math.round(seconds * 1000) : undefined;
|
|
4209
|
-
} catch {
|
|
4210
|
-
return;
|
|
4211
|
-
}
|
|
4212
|
-
}
|
|
4213
|
-
function buildBody2(options) {
|
|
4214
|
-
const body = {};
|
|
4215
|
-
if (options.note !== undefined) {
|
|
4216
|
-
body.note = options.note;
|
|
4217
|
-
}
|
|
4218
|
-
if (options.recordedAt !== undefined) {
|
|
4219
|
-
body.recordedAt = options.recordedAt;
|
|
4220
|
-
}
|
|
4221
|
-
if (options.soundcloudUrl !== undefined) {
|
|
4222
|
-
body.soundcloudUrl = options.soundcloudUrl;
|
|
4223
|
-
}
|
|
4224
|
-
if (options.durationMs !== undefined) {
|
|
4225
|
-
const parsed = parseDuration(options.durationMs);
|
|
4226
|
-
if (parsed === null) {
|
|
4227
|
-
throw new CliError2("invalid_duration", "Duration must be mm:ss or h:mm:ss, or a millisecond count");
|
|
4228
|
-
}
|
|
4229
|
-
body.durationMs = parsed;
|
|
4230
|
-
}
|
|
4231
|
-
return body;
|
|
4232
|
-
}
|
|
4233
|
-
function parseCueFile2(filePath) {
|
|
4234
|
-
if (!existsSync4(filePath)) {
|
|
4235
|
-
throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
|
|
4236
|
-
}
|
|
4237
|
-
const text = readFileSync4(filePath, "utf-8");
|
|
4238
|
-
const trimmed = text.trim();
|
|
4239
|
-
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
4240
|
-
try {
|
|
4241
|
-
const parsed = JSON.parse(trimmed);
|
|
4242
|
-
if (!Array.isArray(parsed)) {
|
|
4243
|
-
throw new Error("not an array");
|
|
4244
|
-
}
|
|
4245
|
-
return parsed.map((entry, index) => {
|
|
4246
|
-
if (typeof entry === "string") {
|
|
4247
|
-
return { ref: entry.trim() };
|
|
4248
|
-
}
|
|
4249
|
-
const obj = entry;
|
|
4250
|
-
if (typeof obj?.ref !== "string") {
|
|
4251
|
-
throw new CliError2("invalid_cue_json", `Entry ${index + 1} missing "ref" string`);
|
|
4252
|
-
}
|
|
4253
|
-
const cue = { ref: obj.ref.trim() };
|
|
4254
|
-
if (typeof obj.startMs === "number" && Number.isInteger(obj.startMs) && obj.startMs >= 0) {
|
|
4255
|
-
cue.startMs = obj.startMs;
|
|
4256
|
-
}
|
|
4257
|
-
return cue;
|
|
4258
|
-
});
|
|
4259
|
-
} catch (error) {
|
|
4260
|
-
if (error instanceof CliError2) {
|
|
4261
|
-
throw error;
|
|
4262
|
-
}
|
|
4263
|
-
throw new CliError2("invalid_cue_json", `Cue JSON parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4264
|
-
}
|
|
4265
|
-
}
|
|
4266
|
-
return parseCueSheet2(text);
|
|
4267
|
-
}
|
|
4268
|
-
function parseCueSheet2(text) {
|
|
4269
|
-
const entries = [];
|
|
4270
|
-
for (const line of text.split(/\r?\n/)) {
|
|
4271
|
-
const trimmed = line.trim();
|
|
4272
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
4273
|
-
continue;
|
|
4274
|
-
}
|
|
4275
|
-
const match = trimmed.match(/^(\d{1,2}:\d{2}(?::\d{2})?)\s+(.+)$/);
|
|
4276
|
-
if (match) {
|
|
4277
|
-
const [, time, ref] = match;
|
|
4278
|
-
if (time === undefined || ref === undefined) {
|
|
4279
|
-
continue;
|
|
4280
|
-
}
|
|
4281
|
-
const startMs = parseDuration(time);
|
|
4282
|
-
if (startMs === null) {
|
|
4283
|
-
continue;
|
|
4284
|
-
}
|
|
4285
|
-
entries.push({ ref: ref.trim(), startMs });
|
|
4286
|
-
} else {
|
|
4287
|
-
entries.push({ ref: trimmed });
|
|
4288
|
-
}
|
|
4289
|
-
}
|
|
4290
|
-
return entries;
|
|
4291
|
-
}
|
|
4292
|
-
var init_mixtapes2 = __esm(() => {
|
|
4293
|
-
init_util();
|
|
4294
|
-
init_api();
|
|
4295
|
-
init_mixtape_api();
|
|
4296
|
-
init_output();
|
|
4297
|
-
});
|
|
4298
|
-
|
|
4299
4017
|
// src/commands/clips.ts
|
|
4300
4018
|
var exports_clips = {};
|
|
4301
4019
|
__export(exports_clips, {
|
|
@@ -4371,9 +4089,6 @@ async function clipsListCommand(filter = {}) {
|
|
|
4371
4089
|
if (filter.recordingId) {
|
|
4372
4090
|
params.set("recordingId", filter.recordingId);
|
|
4373
4091
|
}
|
|
4374
|
-
if (filter.mixtapeId) {
|
|
4375
|
-
params.set("mixtapeId", filter.mixtapeId);
|
|
4376
|
-
}
|
|
4377
4092
|
if (filter.status) {
|
|
4378
4093
|
params.set("status", filter.status);
|
|
4379
4094
|
}
|
|
@@ -4382,28 +4097,17 @@ async function clipsListCommand(filter = {}) {
|
|
|
4382
4097
|
return response.clips;
|
|
4383
4098
|
}
|
|
4384
4099
|
async function resolveClipSource(clip) {
|
|
4385
|
-
if (clip.recordingId) {
|
|
4386
|
-
|
|
4387
|
-
const recording = await recordingGet2(clip.recordingId);
|
|
4388
|
-
if (!recording.r2Key) {
|
|
4389
|
-
throw new CliError2("recording_not_staged", `Recording ${clip.recordingId} has no staged set video`);
|
|
4390
|
-
}
|
|
4391
|
-
return {
|
|
4392
|
-
setUrl: `${FOUND_BASE3}/${recording.r2Key.split("/").map(encodeURIComponent).join("/")}`
|
|
4393
|
-
};
|
|
4394
|
-
}
|
|
4395
|
-
if (!clip.mixtapeId) {
|
|
4396
|
-
throw new CliError2("clip_unlinked", `Clip ${clip.id} is linked to neither a recording nor a mixtape`);
|
|
4397
|
-
}
|
|
4398
|
-
const { mixtapeGetCommand: mixtapeGetCommand2 } = await Promise.resolve().then(() => (init_mixtapes2(), exports_mixtapes2));
|
|
4399
|
-
const mixtape = await mixtapeGetCommand2(clip.mixtapeId);
|
|
4400
|
-
if (!mixtape.logId) {
|
|
4401
|
-
throw new CliError2("mixtape_no_log_id", `Mixtape ${clip.mixtapeId} has no committed Log ID`);
|
|
4100
|
+
if (!clip.recordingId) {
|
|
4101
|
+
throw new CliError2("clip_unlinked", `Clip ${clip.id} is linked to no recording`);
|
|
4402
4102
|
}
|
|
4403
|
-
|
|
4404
|
-
|
|
4103
|
+
const { recordingGet: recordingGet2 } = await Promise.resolve().then(() => (init_recordings(), exports_recordings));
|
|
4104
|
+
const recording = await recordingGet2(clip.recordingId);
|
|
4105
|
+
if (!recording.r2Key) {
|
|
4106
|
+
throw new CliError2("recording_not_staged", `Recording ${clip.recordingId} has no staged set video`);
|
|
4405
4107
|
}
|
|
4406
|
-
return {
|
|
4108
|
+
return {
|
|
4109
|
+
setUrl: `${FOUND_BASE3}/${recording.r2Key.split("/").map(encodeURIComponent).join("/")}`
|
|
4110
|
+
};
|
|
4407
4111
|
}
|
|
4408
4112
|
async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
4409
4113
|
const clips = await clipsListCommand();
|
|
@@ -4497,7 +4201,7 @@ __export(exports_recordings2, {
|
|
|
4497
4201
|
recordingDeleteCommand: () => recordingDeleteCommand2,
|
|
4498
4202
|
recordingCreateCommand: () => recordingCreateCommand2
|
|
4499
4203
|
});
|
|
4500
|
-
import { existsSync as
|
|
4204
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
4501
4205
|
async function recordingGet2(id) {
|
|
4502
4206
|
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4503
4207
|
return response.recording;
|
|
@@ -4566,7 +4270,7 @@ async function recordingCreateCommand2(options = {}) {
|
|
|
4566
4270
|
if (!options.video) {
|
|
4567
4271
|
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
4568
4272
|
}
|
|
4569
|
-
if (!
|
|
4273
|
+
if (!existsSync4(options.video)) {
|
|
4570
4274
|
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
4571
4275
|
}
|
|
4572
4276
|
const created = await adminApiPost("/api/admin/recordings", {
|
|
@@ -4602,11 +4306,11 @@ async function recordingUpdateCommand2(id, options = {}) {
|
|
|
4602
4306
|
body.parentId = options.parentId;
|
|
4603
4307
|
}
|
|
4604
4308
|
if (options.tracklistFile !== undefined) {
|
|
4605
|
-
if (!
|
|
4309
|
+
if (!existsSync4(options.tracklistFile)) {
|
|
4606
4310
|
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
4607
4311
|
}
|
|
4608
4312
|
try {
|
|
4609
|
-
body.tracklistJson = JSON.parse(
|
|
4313
|
+
body.tracklistJson = JSON.parse(readFileSync4(options.tracklistFile, "utf8"));
|
|
4610
4314
|
} catch {
|
|
4611
4315
|
throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
|
|
4612
4316
|
}
|
|
@@ -4651,12 +4355,12 @@ async function recordingReplaceCuesCommand2(id, options = {}) {
|
|
|
4651
4355
|
if (!options.cuesFile) {
|
|
4652
4356
|
throw new CliError2("missing_cues_file", "replace-cues needs a --cues-file <file> (the ordered cue array)");
|
|
4653
4357
|
}
|
|
4654
|
-
if (!
|
|
4358
|
+
if (!existsSync4(options.cuesFile)) {
|
|
4655
4359
|
throw new CliError2("file_not_found", `Cues file not found: ${options.cuesFile}`);
|
|
4656
4360
|
}
|
|
4657
4361
|
let cues;
|
|
4658
4362
|
try {
|
|
4659
|
-
cues = JSON.parse(
|
|
4363
|
+
cues = JSON.parse(readFileSync4(options.cuesFile, "utf8"));
|
|
4660
4364
|
} catch {
|
|
4661
4365
|
throw new CliError2("invalid_cues", `Cues file is not valid JSON: ${options.cuesFile}`);
|
|
4662
4366
|
}
|
|
@@ -4682,8 +4386,8 @@ __export(exports_newsletter, {
|
|
|
4682
4386
|
newsletterDraftCommand: () => newsletterDraftCommand,
|
|
4683
4387
|
newsletterDeleteCommand: () => newsletterDeleteCommand
|
|
4684
4388
|
});
|
|
4685
|
-
import { existsSync as
|
|
4686
|
-
function
|
|
4389
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
4390
|
+
function buildBody2(options, { requireContent }) {
|
|
4687
4391
|
const body = {};
|
|
4688
4392
|
if (options.contentFile !== undefined) {
|
|
4689
4393
|
body.contentJson = readContentFile(options.contentFile);
|
|
@@ -4702,10 +4406,10 @@ function buildBody3(options, { requireContent }) {
|
|
|
4702
4406
|
return body;
|
|
4703
4407
|
}
|
|
4704
4408
|
function readContentFile(filePath) {
|
|
4705
|
-
if (!
|
|
4409
|
+
if (!existsSync5(filePath)) {
|
|
4706
4410
|
throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
|
|
4707
4411
|
}
|
|
4708
|
-
const text =
|
|
4412
|
+
const text = readFileSync5(filePath, "utf-8");
|
|
4709
4413
|
try {
|
|
4710
4414
|
return JSON.parse(text);
|
|
4711
4415
|
} catch (error) {
|
|
@@ -4713,10 +4417,10 @@ function readContentFile(filePath) {
|
|
|
4713
4417
|
}
|
|
4714
4418
|
}
|
|
4715
4419
|
async function newsletterDraftCommand(options) {
|
|
4716
|
-
return adminApiPost("/api/admin/newsletter/editions",
|
|
4420
|
+
return adminApiPost("/api/admin/newsletter/editions", buildBody2(options, { requireContent: true }));
|
|
4717
4421
|
}
|
|
4718
4422
|
async function newsletterUpdateCommand(id, options) {
|
|
4719
|
-
return adminApiPatch(`/api/admin/newsletter/editions/${encodeURIComponent(id)}`,
|
|
4423
|
+
return adminApiPatch(`/api/admin/newsletter/editions/${encodeURIComponent(id)}`, buildBody2(options, { requireContent: false }));
|
|
4720
4424
|
}
|
|
4721
4425
|
async function newsletterSendCommand(id) {
|
|
4722
4426
|
return adminApiPost(`/api/admin/newsletter/editions/${encodeURIComponent(id)}/send`);
|
|
@@ -5340,7 +5044,7 @@ var init_format2 = __esm(() => {
|
|
|
5340
5044
|
});
|
|
5341
5045
|
|
|
5342
5046
|
// src/cli.ts
|
|
5343
|
-
import { existsSync as
|
|
5047
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
5344
5048
|
import path2 from "path";
|
|
5345
5049
|
|
|
5346
5050
|
// ../../node_modules/commander/lib/error.js
|
|
@@ -7506,8 +7210,8 @@ function addListenCommands(program2) {
|
|
|
7506
7210
|
await runRecent(options, recentCommand3);
|
|
7507
7211
|
});
|
|
7508
7212
|
program2.command("mixtapes").description("Fluncle's checkpoint sets").option("--json", "Print JSON", false).action(async (options) => {
|
|
7509
|
-
const { mixtapesCommand:
|
|
7510
|
-
await runMixtapes(options,
|
|
7213
|
+
const { mixtapesCommand: mixtapesCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7214
|
+
await runMixtapes(options, mixtapesCommand2);
|
|
7511
7215
|
});
|
|
7512
7216
|
program2.command("open").description("Pick a track, open it in Spotify").argument("[target]").option("--app", "Open in the native app", false).option("--browser", "Open in the browser", false).option("--limit <limit>", "Number of recent tracks to choose from", "20").allowExcessArguments().action(async (target, options) => {
|
|
7513
7217
|
const openCommands = await Promise.resolve().then(() => (init_open(), exports_open));
|
|
@@ -7669,24 +7373,24 @@ function addAdminCommands(program2) {
|
|
|
7669
7373
|
adminMixtapes.outputHelp();
|
|
7670
7374
|
});
|
|
7671
7375
|
adminMixtapes.command("create").description("Log a new mixtape draft").option("--duration-ms <duration>", "Duration (mm:ss, h:mm:ss, or ms)").option("--json", "Print JSON", false).option("--note <text>", "Operator note").option("--recorded-at <date>", "Recorded date (ISO)").option("--soundcloud-url <url>", "SoundCloud URL (manual; YouTube + Mixcloud come from distribute)").allowExcessArguments().action(async (options) => {
|
|
7672
|
-
const { mixtapeCreateCommand:
|
|
7673
|
-
await runMixtapeCreate(options,
|
|
7376
|
+
const { mixtapeCreateCommand: mixtapeCreateCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7377
|
+
await runMixtapeCreate(options, mixtapeCreateCommand2);
|
|
7674
7378
|
});
|
|
7675
7379
|
adminMixtapes.command("update").description("Update a mixtape's fields").argument("[id]").option("--duration-ms <duration>", "Duration (mm:ss, h:mm:ss, or ms)").option("--json", "Print JSON", false).option("--note <text>", "Operator note").option("--recorded-at <date>", "Recorded date (ISO)").option("--soundcloud-url <url>", "SoundCloud URL (manual; YouTube + Mixcloud come from distribute)").allowExcessArguments().action(async (id, options) => {
|
|
7676
|
-
const { mixtapeUpdateCommand:
|
|
7677
|
-
await runMixtapeUpdate(id, options,
|
|
7380
|
+
const { mixtapeUpdateCommand: mixtapeUpdateCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7381
|
+
await runMixtapeUpdate(id, options, mixtapeUpdateCommand2);
|
|
7678
7382
|
});
|
|
7679
7383
|
adminMixtapes.command("members").description("Set a mixtape's tracklist (refs and/or a cue-sheet file)").argument("[id]").argument("[refs...]").option("--from <file>", "Cue-sheet or JSON file with members").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, refs, options) => {
|
|
7680
|
-
const { mixtapeMembersCommand:
|
|
7681
|
-
await runMixtapeMembers(id, refs, options,
|
|
7384
|
+
const { mixtapeMembersCommand: mixtapeMembersCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7385
|
+
await runMixtapeMembers(id, refs, options, mixtapeMembersCommand2);
|
|
7682
7386
|
});
|
|
7683
7387
|
adminMixtapes.command("publish").description("Publish a mixtape draft").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7684
|
-
const { mixtapePublishCommand:
|
|
7685
|
-
await runMixtapePublish(id, options,
|
|
7388
|
+
const { mixtapePublishCommand: mixtapePublishCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7389
|
+
await runMixtapePublish(id, options, mixtapePublishCommand2);
|
|
7686
7390
|
});
|
|
7687
7391
|
adminMixtapes.command("delete").description("Discard a mixtape draft (published mixtapes can't be deleted)").argument("[id]").option("--json", "Print JSON", false).option("--yes", "Skip confirmation", false).allowExcessArguments().action(async (id, options) => {
|
|
7688
|
-
const { mixtapeDeleteCommand:
|
|
7689
|
-
await runMixtapeDelete(id, options,
|
|
7392
|
+
const { mixtapeDeleteCommand: mixtapeDeleteCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7393
|
+
await runMixtapeDelete(id, options, mixtapeDeleteCommand2);
|
|
7690
7394
|
});
|
|
7691
7395
|
adminMixtapes.command("list").description("List all mixtapes (including drafts)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7692
7396
|
const { mixtapeListCommand: mixtapeListCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
@@ -7696,23 +7400,23 @@ function addAdminCommands(program2) {
|
|
|
7696
7400
|
const { mixtapeGetCommand: mixtapeGetCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7697
7401
|
await runMixtapeGet(idOrLogId, options, mixtapeGetCommand2);
|
|
7698
7402
|
});
|
|
7699
|
-
adminMixtapes.command("distribute").description("
|
|
7700
|
-
const { mixtapeDistributeCommand:
|
|
7701
|
-
await runMixtapeDistribute(idOrLogId, options,
|
|
7403
|
+
adminMixtapes.command("distribute").description("Push a promoted mixtape to YouTube (video) and Mixcloud (audio). The mixtape must already be promoted (`recordings promote`) \u2014 distribute is push-only.").argument("[idOrLogId]").option("--video <file>", "Video file for YouTube").option("--audio <file>", "Audio file for Mixcloud").option("--youtube", "Only distribute to YouTube").option("--mixcloud", "Only distribute to Mixcloud").option("--unlisted", "Keep Mixcloud private too (YouTube is always unlisted until publish-youtube)").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
|
|
7404
|
+
const { mixtapeDistributeCommand: mixtapeDistributeCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7405
|
+
await runMixtapeDistribute(idOrLogId, options, mixtapeDistributeCommand2);
|
|
7702
7406
|
});
|
|
7703
7407
|
adminMixtapes.command("publish-youtube").description("Flip a distributed mixtape's YouTube video from unlisted to public").argument("[idOrLogId]").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
|
|
7704
7408
|
const { publishYoutubeCommand: publishYoutubeCommand3 } = await Promise.resolve().then(() => (init_mixtape_youtube2(), exports_mixtape_youtube2));
|
|
7705
7409
|
await runMixtapePublishYoutube(idOrLogId, options, publishYoutubeCommand3);
|
|
7706
7410
|
});
|
|
7707
7411
|
adminMixtapes.command("resync").description("Re-push a published mixtape's YouTube chapters + Mixcloud sections from its current cues (no re-upload)").argument("[idOrLogId]").option("--youtube", "Only re-sync YouTube").option("--mixcloud", "Only re-sync Mixcloud").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
|
|
7708
|
-
const { mixtapeResyncCommand:
|
|
7709
|
-
await runMixtapeResync(idOrLogId, options,
|
|
7412
|
+
const { mixtapeResyncCommand: mixtapeResyncCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7413
|
+
await runMixtapeResync(idOrLogId, options, mixtapeResyncCommand2);
|
|
7710
7414
|
});
|
|
7711
7415
|
const adminClips = configureCommand(admin.command("clips").description("Mixtape clip (Fluncle Studio) commands"));
|
|
7712
7416
|
adminClips.action(() => {
|
|
7713
7417
|
adminClips.outputHelp();
|
|
7714
7418
|
});
|
|
7715
|
-
adminClips.command("list").description("List clips (filter by --status pending|done
|
|
7419
|
+
adminClips.command("list").description("List clips (filter by --status pending|done and/or --recording <id>)").option("--status <status>", "Filter by cut status (pending|done)").option("--recording <id>", "Filter by recording id").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7716
7420
|
const { clipsListCommand: clipsListCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7717
7421
|
await runClipsList(options, clipsListCommand2);
|
|
7718
7422
|
});
|
|
@@ -7855,7 +7559,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
|
|
|
7855
7559
|
console.log(` mime: ${result.mime}`);
|
|
7856
7560
|
}
|
|
7857
7561
|
async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
|
|
7858
|
-
const script = options.scriptFile ?
|
|
7562
|
+
const script = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
|
|
7859
7563
|
if (!idOrLogId || !script || !script.trim()) {
|
|
7860
7564
|
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]");
|
|
7861
7565
|
}
|
|
@@ -7911,7 +7615,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
|
|
|
7911
7615
|
}
|
|
7912
7616
|
}
|
|
7913
7617
|
async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
|
|
7914
|
-
const note = options.scriptFile ?
|
|
7618
|
+
const note = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
|
|
7915
7619
|
if (!idOrLogId || !note || !note.trim()) {
|
|
7916
7620
|
throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
|
|
7917
7621
|
}
|
|
@@ -8057,7 +7761,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
|
|
|
8057
7761
|
return;
|
|
8058
7762
|
}
|
|
8059
7763
|
const candidate = path2.join(dir, name);
|
|
8060
|
-
return
|
|
7764
|
+
return existsSync6(candidate) ? candidate : undefined;
|
|
8061
7765
|
};
|
|
8062
7766
|
const resolveFile = (explicit, name) => {
|
|
8063
7767
|
if (explicit) {
|
|
@@ -8234,56 +7938,56 @@ async function runTrackPurgeVideo(idOrLogId, options, trackPurgeVideoCommand2) {
|
|
|
8234
7938
|
}
|
|
8235
7939
|
console.log(result.noVideo ? `${result.logId} has no video \u2014 nothing to purge.` : `Purging the stale renditions for ${result.logId} from the edge. The next play picks up the fresh render.`);
|
|
8236
7940
|
}
|
|
8237
|
-
async function runMixtapeCreate(options,
|
|
8238
|
-
const result = await
|
|
7941
|
+
async function runMixtapeCreate(options, mixtapeCreateCommand2) {
|
|
7942
|
+
const result = await mixtapeCreateCommand2(options);
|
|
8239
7943
|
if (options.json) {
|
|
8240
7944
|
printJson(result);
|
|
8241
7945
|
return;
|
|
8242
7946
|
}
|
|
8243
7947
|
console.log(`Logged draft ${result.mixtape.id}. It stays a draft until you publish it.`);
|
|
8244
7948
|
}
|
|
8245
|
-
async function runMixtapeUpdate(id, options,
|
|
7949
|
+
async function runMixtapeUpdate(id, options, mixtapeUpdateCommand2) {
|
|
8246
7950
|
if (!id) {
|
|
8247
7951
|
throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes update <id>");
|
|
8248
7952
|
}
|
|
8249
|
-
const result = await
|
|
7953
|
+
const result = await mixtapeUpdateCommand2(id, options);
|
|
8250
7954
|
if (options.json) {
|
|
8251
7955
|
printJson(result);
|
|
8252
7956
|
return;
|
|
8253
7957
|
}
|
|
8254
7958
|
console.log(`Saved ${result.mixtape.id}.`);
|
|
8255
7959
|
}
|
|
8256
|
-
async function runMixtapeMembers(id, refs, options,
|
|
7960
|
+
async function runMixtapeMembers(id, refs, options, mixtapeMembersCommand2) {
|
|
8257
7961
|
if (!id) {
|
|
8258
7962
|
throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes members <id> [refs...]");
|
|
8259
7963
|
}
|
|
8260
7964
|
if (refs.length === 0 && !options.from) {
|
|
8261
7965
|
throw new Error("Provide refs as arguments or a cue-sheet file with --from");
|
|
8262
7966
|
}
|
|
8263
|
-
const result = await
|
|
7967
|
+
const result = await mixtapeMembersCommand2(id, refs, options);
|
|
8264
7968
|
if (options.json) {
|
|
8265
7969
|
printJson(result);
|
|
8266
7970
|
return;
|
|
8267
7971
|
}
|
|
8268
7972
|
console.log(`Saved the tracklist: ${result.mixtape.memberCount} bangers on ${result.mixtape.id}.`);
|
|
8269
7973
|
}
|
|
8270
|
-
async function runMixtapePublish(id, options,
|
|
7974
|
+
async function runMixtapePublish(id, options, mixtapePublishCommand2) {
|
|
8271
7975
|
if (!id) {
|
|
8272
7976
|
throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes publish <id>");
|
|
8273
7977
|
}
|
|
8274
|
-
const result = await
|
|
7978
|
+
const result = await mixtapePublishCommand2(id);
|
|
8275
7979
|
if (options.json) {
|
|
8276
7980
|
printJson(result);
|
|
8277
7981
|
return;
|
|
8278
7982
|
}
|
|
8279
7983
|
console.log(`Minted ${result.mixtape.logId} (fluncle://${result.mixtape.logId}) \u2014 distributing. ` + "Run `distribute` to push it to the platforms.");
|
|
8280
7984
|
}
|
|
8281
|
-
async function runMixtapeDistribute(idOrLogId, options,
|
|
7985
|
+
async function runMixtapeDistribute(idOrLogId, options, mixtapeDistributeCommand2) {
|
|
8282
7986
|
if (!idOrLogId) {
|
|
8283
7987
|
throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes distribute <idOrLogId> --video <mp4> --audio <file>");
|
|
8284
7988
|
}
|
|
8285
7989
|
const onProgress = options.json ? () => {} : (message) => console.log(message);
|
|
8286
|
-
const result = await
|
|
7990
|
+
const result = await mixtapeDistributeCommand2(idOrLogId, options, onProgress);
|
|
8287
7991
|
if (options.json) {
|
|
8288
7992
|
printJson(result);
|
|
8289
7993
|
return;
|
|
@@ -8304,12 +8008,12 @@ async function runMixtapePublishYoutube(idOrLogId, options, publishYoutubeComman
|
|
|
8304
8008
|
}
|
|
8305
8009
|
console.log(`YouTube video is now public: ${result.url}`);
|
|
8306
8010
|
}
|
|
8307
|
-
async function runMixtapeResync(idOrLogId, options,
|
|
8011
|
+
async function runMixtapeResync(idOrLogId, options, mixtapeResyncCommand2) {
|
|
8308
8012
|
if (!idOrLogId) {
|
|
8309
8013
|
throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes resync <idOrLogId>");
|
|
8310
8014
|
}
|
|
8311
8015
|
const onProgress = options.json ? () => {} : (message) => console.log(message);
|
|
8312
|
-
const result = await
|
|
8016
|
+
const result = await mixtapeResyncCommand2(idOrLogId, options, onProgress);
|
|
8313
8017
|
if (options.json) {
|
|
8314
8018
|
printJson(result);
|
|
8315
8019
|
return;
|
|
@@ -8319,14 +8023,14 @@ async function runMixtapeResync(idOrLogId, options, mixtapeResyncCommand3) {
|
|
|
8319
8023
|
console.log(`Re-synced ${result.logId} (fluncle://${result.logId}).
|
|
8320
8024
|
${links}`);
|
|
8321
8025
|
}
|
|
8322
|
-
async function runMixtapeDelete(id, options,
|
|
8026
|
+
async function runMixtapeDelete(id, options, mixtapeDeleteCommand2) {
|
|
8323
8027
|
if (!id) {
|
|
8324
8028
|
throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes delete <id> --yes");
|
|
8325
8029
|
}
|
|
8326
8030
|
if (!options.yes) {
|
|
8327
8031
|
throw new Error("Pass --yes to confirm the discard. Published mixtapes can't be deleted.");
|
|
8328
8032
|
}
|
|
8329
|
-
await
|
|
8033
|
+
await mixtapeDeleteCommand2(id);
|
|
8330
8034
|
if (options.json) {
|
|
8331
8035
|
printJson({ id, ok: true });
|
|
8332
8036
|
return;
|
|
@@ -8351,7 +8055,6 @@ async function runMixtapeList(options, mixtapeListCommand2) {
|
|
|
8351
8055
|
}
|
|
8352
8056
|
async function runClipsList(options, clipsListCommand2) {
|
|
8353
8057
|
const clips = await clipsListCommand2({
|
|
8354
|
-
mixtapeId: options.mixtape,
|
|
8355
8058
|
recordingId: options.recording,
|
|
8356
8059
|
status: options.status
|
|
8357
8060
|
});
|
|
@@ -8364,7 +8067,7 @@ async function runClipsList(options, clipsListCommand2) {
|
|
|
8364
8067
|
return;
|
|
8365
8068
|
}
|
|
8366
8069
|
for (const clip of clips) {
|
|
8367
|
-
const source = clip.recordingId ??
|
|
8070
|
+
const source = clip.recordingId ?? "\u2014";
|
|
8368
8071
|
console.log(`${clip.id} ${clip.status} ${source} ${clip.inMs}-${clip.outMs}ms x=${clip.xOffset}`);
|
|
8369
8072
|
}
|
|
8370
8073
|
}
|
|
@@ -8527,8 +8230,8 @@ async function runRecent(options, recentCommand3) {
|
|
|
8527
8230
|
console.log(trackRows2(tracks).join(`
|
|
8528
8231
|
`));
|
|
8529
8232
|
}
|
|
8530
|
-
async function runMixtapes(options,
|
|
8531
|
-
const mixtapes = await
|
|
8233
|
+
async function runMixtapes(options, mixtapesCommand2) {
|
|
8234
|
+
const mixtapes = await mixtapesCommand2();
|
|
8532
8235
|
if (options.json) {
|
|
8533
8236
|
printJson({ mixtapes, ok: true });
|
|
8534
8237
|
return;
|
package/package.json
CHANGED