fluncle 0.73.0 → 0.75.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 +501 -450
- package/package.json +1 -1
package/bin/fluncle.mjs
CHANGED
|
@@ -524,7 +524,7 @@ function parseVersion(version) {
|
|
|
524
524
|
var currentVersion;
|
|
525
525
|
var init_version = __esm(() => {
|
|
526
526
|
init_output();
|
|
527
|
-
currentVersion = "0.
|
|
527
|
+
currentVersion = "0.75.0".trim() ? "0.75.0".trim() : "0.1.0";
|
|
528
528
|
});
|
|
529
529
|
|
|
530
530
|
// src/update-notifier.ts
|
|
@@ -988,6 +988,24 @@ var init_recent = __esm(() => {
|
|
|
988
988
|
init_api();
|
|
989
989
|
});
|
|
990
990
|
|
|
991
|
+
// src/commands/mixtape-api.ts
|
|
992
|
+
async function mixtapeListCommand() {
|
|
993
|
+
const response = await adminApiGet("/api/admin/mixtapes");
|
|
994
|
+
return response.mixtapes;
|
|
995
|
+
}
|
|
996
|
+
async function mixtapeGetCommand(idOrLogId) {
|
|
997
|
+
const mixtapes = await mixtapeListCommand();
|
|
998
|
+
const match = mixtapes.find((mixtape) => mixtape.id === idOrLogId || mixtape.logId === idOrLogId);
|
|
999
|
+
if (!match) {
|
|
1000
|
+
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
1001
|
+
}
|
|
1002
|
+
return match;
|
|
1003
|
+
}
|
|
1004
|
+
var init_mixtape_api = __esm(() => {
|
|
1005
|
+
init_api();
|
|
1006
|
+
init_output();
|
|
1007
|
+
});
|
|
1008
|
+
|
|
991
1009
|
// src/commands/mixtape-youtube.ts
|
|
992
1010
|
var exports_mixtape_youtube = {};
|
|
993
1011
|
__export(exports_mixtape_youtube, {
|
|
@@ -1148,6 +1166,153 @@ var init_mixtape_youtube = __esm(() => {
|
|
|
1148
1166
|
init_output();
|
|
1149
1167
|
});
|
|
1150
1168
|
|
|
1169
|
+
// src/commands/mixtape-mixcloud.ts
|
|
1170
|
+
var exports_mixtape_mixcloud = {};
|
|
1171
|
+
__export(exports_mixtape_mixcloud, {
|
|
1172
|
+
mixtapeDescription: () => mixtapeDescription,
|
|
1173
|
+
mixcloudSections: () => mixcloudSections,
|
|
1174
|
+
distributeMixcloud: () => distributeMixcloud,
|
|
1175
|
+
authMixcloudCommand: () => authMixcloudCommand
|
|
1176
|
+
});
|
|
1177
|
+
async function distributeMixcloud(mixtapeId, audioPath, onProgress, unlisted = false) {
|
|
1178
|
+
const token = await fetchMixcloudToken();
|
|
1179
|
+
const mixtape = await mixtapeGetCommand(mixtapeId);
|
|
1180
|
+
const logId = mixtape.logId;
|
|
1181
|
+
if (!logId) {
|
|
1182
|
+
throw new CliError2("mixtape_no_log_id", "The mixtape has no Log ID; mint it before distributing");
|
|
1183
|
+
}
|
|
1184
|
+
const audio = Bun.file(audioPath);
|
|
1185
|
+
if (!await audio.exists()) {
|
|
1186
|
+
throw new CliError2("audio_not_found", `Audio master not found: ${audioPath}`);
|
|
1187
|
+
}
|
|
1188
|
+
const form = new FormData;
|
|
1189
|
+
form.append("mp3", audio);
|
|
1190
|
+
form.append("name", mixtape.title);
|
|
1191
|
+
form.append("description", mixtapeDescription(mixtape.note, logId));
|
|
1192
|
+
onProgress?.("Mixcloud: fetching the cover…");
|
|
1193
|
+
const picture = await fetchCover(logId);
|
|
1194
|
+
if (picture) {
|
|
1195
|
+
form.append("picture", picture, "cover.png");
|
|
1196
|
+
}
|
|
1197
|
+
for (const [index, tag] of mixtapeTags(mixtape).entries()) {
|
|
1198
|
+
form.append(`tags-${index}-tag`, tag);
|
|
1199
|
+
}
|
|
1200
|
+
const sections = mixcloudSections(mixtape.members);
|
|
1201
|
+
for (const [index, section] of sections.entries()) {
|
|
1202
|
+
form.append(`sections-${index}-artist`, section.artist);
|
|
1203
|
+
form.append(`sections-${index}-song`, section.song);
|
|
1204
|
+
form.append(`sections-${index}-start_time`, String(section.start_time));
|
|
1205
|
+
}
|
|
1206
|
+
if (unlisted) {
|
|
1207
|
+
form.append("unlisted", "1");
|
|
1208
|
+
onProgress?.("Mixcloud: uploading UNLISTED (private).");
|
|
1209
|
+
}
|
|
1210
|
+
const cuelessCount = mixtape.members.length - sections.length;
|
|
1211
|
+
if (cuelessCount > 0) {
|
|
1212
|
+
onProgress?.(`Mixcloud: ${cuelessCount} of ${mixtape.members.length} members have no cue (omitted from sections).`);
|
|
1213
|
+
}
|
|
1214
|
+
onProgress?.("Mixcloud: uploading the master…");
|
|
1215
|
+
const uploadResponse = await fetch(`${MIXCLOUD_API}/upload/?access_token=${encodeURIComponent(token)}`, {
|
|
1216
|
+
body: form,
|
|
1217
|
+
method: "POST"
|
|
1218
|
+
});
|
|
1219
|
+
const uploadText = await uploadResponse.text();
|
|
1220
|
+
if (!uploadResponse.ok) {
|
|
1221
|
+
throwMixcloudError(uploadResponse.status, uploadText);
|
|
1222
|
+
}
|
|
1223
|
+
const result = parseUploadResult(uploadText);
|
|
1224
|
+
if (!result.success || !result.key) {
|
|
1225
|
+
throw new CliError2("mixcloud_upload_rejected", `Mixcloud rejected the upload: ${result.message ?? uploadText.slice(0, 300)}`);
|
|
1226
|
+
}
|
|
1227
|
+
const externalId = result.key;
|
|
1228
|
+
const url = `https://www.mixcloud.com${result.key}`;
|
|
1229
|
+
onProgress?.("Mixcloud: recording the link…");
|
|
1230
|
+
await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/finalize`, {
|
|
1231
|
+
externalId,
|
|
1232
|
+
url
|
|
1233
|
+
});
|
|
1234
|
+
return { url };
|
|
1235
|
+
}
|
|
1236
|
+
async function authMixcloudCommand() {
|
|
1237
|
+
const response = await adminApiGet("/api/admin/mixcloud/auth/start");
|
|
1238
|
+
console.log(`Open this Mixcloud authorization URL:
|
|
1239
|
+
|
|
1240
|
+
${response.authUrl}
|
|
1241
|
+
|
|
1242
|
+
After approving access, Mixcloud returns to the Fluncle admin callback and stores the access token server-side.`);
|
|
1243
|
+
}
|
|
1244
|
+
function mixtapeDescription(note, logId) {
|
|
1245
|
+
const breadcrumb = `fluncle://${logId}`;
|
|
1246
|
+
const body = (note ?? "").trim();
|
|
1247
|
+
const full = body ? `${body}
|
|
1248
|
+
|
|
1249
|
+
${breadcrumb}` : breadcrumb;
|
|
1250
|
+
if (full.length <= DESCRIPTION_MAX) {
|
|
1251
|
+
return full;
|
|
1252
|
+
}
|
|
1253
|
+
const room = DESCRIPTION_MAX - (breadcrumb.length + 2);
|
|
1254
|
+
const trimmedNote = body.slice(0, Math.max(room, 0)).trimEnd();
|
|
1255
|
+
return trimmedNote ? `${trimmedNote}
|
|
1256
|
+
|
|
1257
|
+
${breadcrumb}` : breadcrumb;
|
|
1258
|
+
}
|
|
1259
|
+
function mixcloudSections(members) {
|
|
1260
|
+
return members.filter((member) => typeof member.startMs === "number").sort((a, b) => a.startMs - b.startMs).map((member) => ({
|
|
1261
|
+
artist: member.artists.join(", "),
|
|
1262
|
+
song: member.title,
|
|
1263
|
+
start_time: Math.floor(member.startMs / 1000)
|
|
1264
|
+
}));
|
|
1265
|
+
}
|
|
1266
|
+
function mixtapeTags(_mixtape) {
|
|
1267
|
+
return ["Drum & Bass", "Fluncle"];
|
|
1268
|
+
}
|
|
1269
|
+
async function fetchMixcloudToken() {
|
|
1270
|
+
try {
|
|
1271
|
+
const response = await adminApiPost("/api/admin/mixcloud/token");
|
|
1272
|
+
return response.accessToken;
|
|
1273
|
+
} catch {
|
|
1274
|
+
throw new CliError2("mixcloud_not_connected", "Mixcloud is not connected. Run `fluncle admin auth mixcloud` to authorize it.");
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
async function fetchCover(logId) {
|
|
1278
|
+
for (const size of ["square", "og"]) {
|
|
1279
|
+
const response = await fetch(`${COVER_BASE}/${encodeURIComponent(logId)}?size=${size}`);
|
|
1280
|
+
if (!response.ok) {
|
|
1281
|
+
continue;
|
|
1282
|
+
}
|
|
1283
|
+
const blob = await response.blob();
|
|
1284
|
+
if (blob.size <= PICTURE_MAX_BYTES) {
|
|
1285
|
+
return blob;
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
return;
|
|
1289
|
+
}
|
|
1290
|
+
function parseUploadResult(body) {
|
|
1291
|
+
try {
|
|
1292
|
+
const data = JSON.parse(body);
|
|
1293
|
+
return {
|
|
1294
|
+
key: data.result?.key,
|
|
1295
|
+
message: data.result?.message,
|
|
1296
|
+
success: data.result?.success === true
|
|
1297
|
+
};
|
|
1298
|
+
} catch {
|
|
1299
|
+
return { message: body.slice(0, 300), success: false };
|
|
1300
|
+
}
|
|
1301
|
+
}
|
|
1302
|
+
function throwMixcloudError(status, body) {
|
|
1303
|
+
if (body.includes("An invalid access token was provided")) {
|
|
1304
|
+
throw new CliError2("mixcloud_invalid_token", "Mixcloud rejected the access token. Re-auth with `fluncle admin auth mixcloud`.");
|
|
1305
|
+
}
|
|
1306
|
+
throw new CliError2("mixcloud_request_failed", `Mixcloud responded ${status}${body ? `: ${body.slice(0, 300)}` : ""}`);
|
|
1307
|
+
}
|
|
1308
|
+
var MIXCLOUD_API = "https://api.mixcloud.com", DESCRIPTION_MAX = 1000, PICTURE_MAX_BYTES, COVER_BASE = "https://www.fluncle.com/api/mixtape-cover";
|
|
1309
|
+
var init_mixtape_mixcloud = __esm(() => {
|
|
1310
|
+
init_api();
|
|
1311
|
+
init_output();
|
|
1312
|
+
init_mixtape_api();
|
|
1313
|
+
PICTURE_MAX_BYTES = 10 * 1024 * 1024;
|
|
1314
|
+
});
|
|
1315
|
+
|
|
1151
1316
|
// src/commands/mixtape-set-video.ts
|
|
1152
1317
|
var exports_mixtape_set_video = {};
|
|
1153
1318
|
__export(exports_mixtape_set_video, {
|
|
@@ -1353,389 +1518,11 @@ async function mixtapeMembersCommand(id, refs, options) {
|
|
|
1353
1518
|
async function mixtapePublishCommand(id) {
|
|
1354
1519
|
return adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(id)}/publish`);
|
|
1355
1520
|
}
|
|
1356
|
-
async function mixtapeDeleteCommand(id) {
|
|
1357
|
-
return adminApiDelete(`/api/admin/mixtapes/${encodeURIComponent(id)}`);
|
|
1358
|
-
}
|
|
1359
|
-
async function
|
|
1360
|
-
const
|
|
1361
|
-
return response.mixtapes;
|
|
1362
|
-
}
|
|
1363
|
-
async function mixtapeGetCommand(idOrLogId) {
|
|
1364
|
-
const mixtapes = await mixtapeListCommand();
|
|
1365
|
-
const match = mixtapes.find((mixtape) => mixtape.id === idOrLogId || mixtape.logId === idOrLogId);
|
|
1366
|
-
if (!match) {
|
|
1367
|
-
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
1368
|
-
}
|
|
1369
|
-
return match;
|
|
1370
|
-
}
|
|
1371
|
-
async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {}) {
|
|
1372
|
-
const mixtape = await mixtapeGetCommand(idOrLogId);
|
|
1373
|
-
if (!mixtape.id) {
|
|
1374
|
-
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
1375
|
-
}
|
|
1376
|
-
const both = !options.youtube && !options.mixcloud && !options.setVideo;
|
|
1377
|
-
const doYoutube = both || Boolean(options.youtube);
|
|
1378
|
-
const doMixcloud = both || Boolean(options.mixcloud);
|
|
1379
|
-
const doSetVideo = Boolean(options.setVideo);
|
|
1380
|
-
if (doYoutube && !options.video) {
|
|
1381
|
-
throw new CliError2("missing_video", "YouTube distribution needs --video <mp4>");
|
|
1382
|
-
}
|
|
1383
|
-
if (doMixcloud && !options.audio) {
|
|
1384
|
-
throw new CliError2("missing_audio", "Mixcloud distribution needs --audio <file>");
|
|
1385
|
-
}
|
|
1386
|
-
if (doSetVideo && !options.video) {
|
|
1387
|
-
throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
|
|
1388
|
-
}
|
|
1389
|
-
const mixtapeId = mixtape.id;
|
|
1390
|
-
let logId = mixtape.logId;
|
|
1391
|
-
if (!mixtape.durationMs) {
|
|
1392
|
-
const source = options.audio ?? options.video;
|
|
1393
|
-
const durationMs = source ? await probeDurationMs(source) : undefined;
|
|
1394
|
-
if (durationMs) {
|
|
1395
|
-
await mixtapeUpdateCommand(mixtapeId, { durationMs: String(durationMs), json: false });
|
|
1396
|
-
onProgress(`Duration: ${Math.round(durationMs / 60000)} min (from the upload).`);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
if (mixtape.status === "draft") {
|
|
1400
|
-
onProgress("Minting the coordinate…");
|
|
1401
|
-
const published = await mixtapePublishCommand(mixtapeId);
|
|
1402
|
-
logId = published.mixtape.logId;
|
|
1403
|
-
onProgress(`Minted ${logId}.`);
|
|
1404
|
-
} else if (mixtape.status === "published") {
|
|
1405
|
-
onProgress(`Already published (${logId}); re-distributing.`);
|
|
1406
|
-
} else {
|
|
1407
|
-
onProgress(`Resuming distribution for ${logId}.`);
|
|
1408
|
-
}
|
|
1409
|
-
if (!logId) {
|
|
1410
|
-
throw new CliError2("mint_failed", "The mixtape has no Log ID after minting");
|
|
1411
|
-
}
|
|
1412
|
-
const results = [];
|
|
1413
|
-
if (doYoutube) {
|
|
1414
|
-
if (!options.video) {
|
|
1415
|
-
throw new CliError2("missing_video", "YouTube distribution needs --video <mp4>");
|
|
1416
|
-
}
|
|
1417
|
-
onProgress("YouTube: uploading video…");
|
|
1418
|
-
const { distributeYoutube: distributeYoutube2 } = await Promise.resolve().then(() => (init_mixtape_youtube(), exports_mixtape_youtube));
|
|
1419
|
-
const result = await distributeYoutube2(mixtapeId, options.video, onProgress);
|
|
1420
|
-
results.push({ platform: "youtube", url: result.url });
|
|
1421
|
-
onProgress(`YouTube: ${result.url}`);
|
|
1422
|
-
}
|
|
1423
|
-
if (doMixcloud) {
|
|
1424
|
-
if (!options.audio) {
|
|
1425
|
-
throw new CliError2("missing_audio", "Mixcloud distribution needs --audio <file>");
|
|
1426
|
-
}
|
|
1427
|
-
onProgress("Mixcloud: uploading audio…");
|
|
1428
|
-
const { distributeMixcloud } = await Promise.resolve().then(() => (init_mixtape_mixcloud(), exports_mixtape_mixcloud));
|
|
1429
|
-
const result = await distributeMixcloud(mixtapeId, options.audio, onProgress, options.unlisted);
|
|
1430
|
-
results.push({ platform: "mixcloud", url: result.url });
|
|
1431
|
-
onProgress(`Mixcloud: ${result.url}`);
|
|
1432
|
-
}
|
|
1433
|
-
if (doSetVideo) {
|
|
1434
|
-
if (!options.video) {
|
|
1435
|
-
throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
|
|
1436
|
-
}
|
|
1437
|
-
onProgress("Set video: staging the 1080p rendition…");
|
|
1438
|
-
const { stageSetVideo: stageSetVideo2 } = await Promise.resolve().then(() => (init_mixtape_set_video(), exports_mixtape_set_video));
|
|
1439
|
-
const result = await stageSetVideo2(mixtapeId, options.video, onProgress);
|
|
1440
|
-
results.push({ platform: "set-video", url: result.url });
|
|
1441
|
-
onProgress(`Set video: ${result.url}`);
|
|
1442
|
-
}
|
|
1443
|
-
return { logId, mixtapeId, results };
|
|
1444
|
-
}
|
|
1445
|
-
async function probeDurationMs(filePath) {
|
|
1446
|
-
try {
|
|
1447
|
-
const proc = Bun.spawn(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", filePath], { stderr: "ignore", stdout: "pipe" });
|
|
1448
|
-
const out = await new Response(proc.stdout).text();
|
|
1449
|
-
await proc.exited;
|
|
1450
|
-
const seconds = Number.parseFloat(out.trim());
|
|
1451
|
-
return Number.isFinite(seconds) && seconds > 0 ? Math.round(seconds * 1000) : undefined;
|
|
1452
|
-
} catch {
|
|
1453
|
-
return;
|
|
1454
|
-
}
|
|
1455
|
-
}
|
|
1456
|
-
function buildBody(options) {
|
|
1457
|
-
const body = {};
|
|
1458
|
-
if (options.note !== undefined) {
|
|
1459
|
-
body.note = options.note;
|
|
1460
|
-
}
|
|
1461
|
-
if (options.recordedAt !== undefined) {
|
|
1462
|
-
body.recordedAt = options.recordedAt;
|
|
1463
|
-
}
|
|
1464
|
-
if (options.soundcloudUrl !== undefined) {
|
|
1465
|
-
body.soundcloudUrl = options.soundcloudUrl;
|
|
1466
|
-
}
|
|
1467
|
-
if (options.durationMs !== undefined) {
|
|
1468
|
-
const parsed = parseDuration(options.durationMs);
|
|
1469
|
-
if (parsed === null) {
|
|
1470
|
-
throw new CliError2("invalid_duration", "Duration must be mm:ss or h:mm:ss, or a millisecond count");
|
|
1471
|
-
}
|
|
1472
|
-
body.durationMs = parsed;
|
|
1473
|
-
}
|
|
1474
|
-
return body;
|
|
1475
|
-
}
|
|
1476
|
-
function parseCueFile(filePath) {
|
|
1477
|
-
if (!existsSync2(filePath)) {
|
|
1478
|
-
throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
|
|
1479
|
-
}
|
|
1480
|
-
const text = readFileSync2(filePath, "utf-8");
|
|
1481
|
-
const trimmed = text.trim();
|
|
1482
|
-
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
1483
|
-
try {
|
|
1484
|
-
const parsed = JSON.parse(trimmed);
|
|
1485
|
-
if (!Array.isArray(parsed)) {
|
|
1486
|
-
throw new Error("not an array");
|
|
1487
|
-
}
|
|
1488
|
-
return parsed.map((entry, index) => {
|
|
1489
|
-
if (typeof entry === "string") {
|
|
1490
|
-
return { ref: entry.trim() };
|
|
1491
|
-
}
|
|
1492
|
-
const obj = entry;
|
|
1493
|
-
if (typeof obj?.ref !== "string") {
|
|
1494
|
-
throw new CliError2("invalid_cue_json", `Entry ${index + 1} missing "ref" string`);
|
|
1495
|
-
}
|
|
1496
|
-
const cue = { ref: obj.ref.trim() };
|
|
1497
|
-
if (typeof obj.startMs === "number" && Number.isInteger(obj.startMs) && obj.startMs >= 0) {
|
|
1498
|
-
cue.startMs = obj.startMs;
|
|
1499
|
-
}
|
|
1500
|
-
return cue;
|
|
1501
|
-
});
|
|
1502
|
-
} catch (error) {
|
|
1503
|
-
if (error instanceof CliError2) {
|
|
1504
|
-
throw error;
|
|
1505
|
-
}
|
|
1506
|
-
throw new CliError2("invalid_cue_json", `Cue JSON parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1507
|
-
}
|
|
1508
|
-
}
|
|
1509
|
-
return parseCueSheet(text);
|
|
1510
|
-
}
|
|
1511
|
-
function parseCueSheet(text) {
|
|
1512
|
-
const entries = [];
|
|
1513
|
-
for (const line of text.split(/\r?\n/)) {
|
|
1514
|
-
const trimmed = line.trim();
|
|
1515
|
-
if (!trimmed || trimmed.startsWith("#")) {
|
|
1516
|
-
continue;
|
|
1517
|
-
}
|
|
1518
|
-
const match = trimmed.match(/^(\d{1,2}:\d{2}(?::\d{2})?)\s+(.+)$/);
|
|
1519
|
-
if (match) {
|
|
1520
|
-
const [, time, ref] = match;
|
|
1521
|
-
if (time === undefined || ref === undefined) {
|
|
1522
|
-
continue;
|
|
1523
|
-
}
|
|
1524
|
-
const startMs = parseDuration(time);
|
|
1525
|
-
if (startMs === null) {
|
|
1526
|
-
continue;
|
|
1527
|
-
}
|
|
1528
|
-
entries.push({ ref: ref.trim(), startMs });
|
|
1529
|
-
} else {
|
|
1530
|
-
entries.push({ ref: trimmed });
|
|
1531
|
-
}
|
|
1532
|
-
}
|
|
1533
|
-
return entries;
|
|
1534
|
-
}
|
|
1535
|
-
var init_mixtapes = __esm(() => {
|
|
1536
|
-
init_util();
|
|
1537
|
-
init_api();
|
|
1538
|
-
init_output();
|
|
1539
|
-
});
|
|
1540
|
-
|
|
1541
|
-
// src/commands/mixtape-mixcloud.ts
|
|
1542
|
-
var exports_mixtape_mixcloud = {};
|
|
1543
|
-
__export(exports_mixtape_mixcloud, {
|
|
1544
|
-
mixtapeDescription: () => mixtapeDescription,
|
|
1545
|
-
mixcloudSections: () => mixcloudSections,
|
|
1546
|
-
distributeMixcloud: () => distributeMixcloud,
|
|
1547
|
-
authMixcloudCommand: () => authMixcloudCommand
|
|
1548
|
-
});
|
|
1549
|
-
async function distributeMixcloud(mixtapeId, audioPath, onProgress, unlisted = false) {
|
|
1550
|
-
const token = await fetchMixcloudToken();
|
|
1551
|
-
const mixtape = await mixtapeGetCommand(mixtapeId);
|
|
1552
|
-
const logId = mixtape.logId;
|
|
1553
|
-
if (!logId) {
|
|
1554
|
-
throw new CliError2("mixtape_no_log_id", "The mixtape has no Log ID; mint it before distributing");
|
|
1555
|
-
}
|
|
1556
|
-
const audio = Bun.file(audioPath);
|
|
1557
|
-
if (!await audio.exists()) {
|
|
1558
|
-
throw new CliError2("audio_not_found", `Audio master not found: ${audioPath}`);
|
|
1559
|
-
}
|
|
1560
|
-
const form = new FormData;
|
|
1561
|
-
form.append("mp3", audio);
|
|
1562
|
-
form.append("name", mixtape.title);
|
|
1563
|
-
form.append("description", mixtapeDescription(mixtape.note, logId));
|
|
1564
|
-
onProgress?.("Mixcloud: fetching the cover…");
|
|
1565
|
-
const picture = await fetchCover(logId);
|
|
1566
|
-
if (picture) {
|
|
1567
|
-
form.append("picture", picture, "cover.png");
|
|
1568
|
-
}
|
|
1569
|
-
for (const [index, tag] of mixtapeTags(mixtape).entries()) {
|
|
1570
|
-
form.append(`tags-${index}-tag`, tag);
|
|
1571
|
-
}
|
|
1572
|
-
const sections = mixcloudSections(mixtape.members);
|
|
1573
|
-
for (const [index, section] of sections.entries()) {
|
|
1574
|
-
form.append(`sections-${index}-artist`, section.artist);
|
|
1575
|
-
form.append(`sections-${index}-song`, section.song);
|
|
1576
|
-
form.append(`sections-${index}-start_time`, String(section.start_time));
|
|
1577
|
-
}
|
|
1578
|
-
if (unlisted) {
|
|
1579
|
-
form.append("unlisted", "1");
|
|
1580
|
-
onProgress?.("Mixcloud: uploading UNLISTED (private).");
|
|
1581
|
-
}
|
|
1582
|
-
const cuelessCount = mixtape.members.length - sections.length;
|
|
1583
|
-
if (cuelessCount > 0) {
|
|
1584
|
-
onProgress?.(`Mixcloud: ${cuelessCount} of ${mixtape.members.length} members have no cue (omitted from sections).`);
|
|
1585
|
-
}
|
|
1586
|
-
onProgress?.("Mixcloud: uploading the master…");
|
|
1587
|
-
const uploadResponse = await fetch(`${MIXCLOUD_API}/upload/?access_token=${encodeURIComponent(token)}`, {
|
|
1588
|
-
body: form,
|
|
1589
|
-
method: "POST"
|
|
1590
|
-
});
|
|
1591
|
-
const uploadText = await uploadResponse.text();
|
|
1592
|
-
if (!uploadResponse.ok) {
|
|
1593
|
-
throwMixcloudError(uploadResponse.status, uploadText);
|
|
1594
|
-
}
|
|
1595
|
-
const result = parseUploadResult(uploadText);
|
|
1596
|
-
if (!result.success || !result.key) {
|
|
1597
|
-
throw new CliError2("mixcloud_upload_rejected", `Mixcloud rejected the upload: ${result.message ?? uploadText.slice(0, 300)}`);
|
|
1598
|
-
}
|
|
1599
|
-
const externalId = result.key;
|
|
1600
|
-
const url = `https://www.mixcloud.com${result.key}`;
|
|
1601
|
-
onProgress?.("Mixcloud: recording the link…");
|
|
1602
|
-
await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/finalize`, {
|
|
1603
|
-
externalId,
|
|
1604
|
-
url
|
|
1605
|
-
});
|
|
1606
|
-
return { url };
|
|
1607
|
-
}
|
|
1608
|
-
async function authMixcloudCommand() {
|
|
1609
|
-
const response = await adminApiGet("/api/admin/mixcloud/auth/start");
|
|
1610
|
-
console.log(`Open this Mixcloud authorization URL:
|
|
1611
|
-
|
|
1612
|
-
${response.authUrl}
|
|
1613
|
-
|
|
1614
|
-
After approving access, Mixcloud returns to the Fluncle admin callback and stores the access token server-side.`);
|
|
1615
|
-
}
|
|
1616
|
-
function mixtapeDescription(note, logId) {
|
|
1617
|
-
const breadcrumb = `fluncle://${logId}`;
|
|
1618
|
-
const body = (note ?? "").trim();
|
|
1619
|
-
const full = body ? `${body}
|
|
1620
|
-
|
|
1621
|
-
${breadcrumb}` : breadcrumb;
|
|
1622
|
-
if (full.length <= DESCRIPTION_MAX) {
|
|
1623
|
-
return full;
|
|
1624
|
-
}
|
|
1625
|
-
const room = DESCRIPTION_MAX - (breadcrumb.length + 2);
|
|
1626
|
-
const trimmedNote = body.slice(0, Math.max(room, 0)).trimEnd();
|
|
1627
|
-
return trimmedNote ? `${trimmedNote}
|
|
1628
|
-
|
|
1629
|
-
${breadcrumb}` : breadcrumb;
|
|
1630
|
-
}
|
|
1631
|
-
function mixcloudSections(members) {
|
|
1632
|
-
return members.filter((member) => typeof member.startMs === "number").sort((a, b) => a.startMs - b.startMs).map((member) => ({
|
|
1633
|
-
artist: member.artists.join(", "),
|
|
1634
|
-
song: member.title,
|
|
1635
|
-
start_time: Math.floor(member.startMs / 1000)
|
|
1636
|
-
}));
|
|
1637
|
-
}
|
|
1638
|
-
function mixtapeTags(_mixtape) {
|
|
1639
|
-
return ["Drum & Bass", "Fluncle"];
|
|
1640
|
-
}
|
|
1641
|
-
async function fetchMixcloudToken() {
|
|
1642
|
-
try {
|
|
1643
|
-
const response = await adminApiPost("/api/admin/mixcloud/token");
|
|
1644
|
-
return response.accessToken;
|
|
1645
|
-
} catch {
|
|
1646
|
-
throw new CliError2("mixcloud_not_connected", "Mixcloud is not connected. Run `fluncle admin auth mixcloud` to authorize it.");
|
|
1647
|
-
}
|
|
1648
|
-
}
|
|
1649
|
-
async function fetchCover(logId) {
|
|
1650
|
-
for (const size of ["square", "og"]) {
|
|
1651
|
-
const response = await fetch(`${COVER_BASE}/${encodeURIComponent(logId)}?size=${size}`);
|
|
1652
|
-
if (!response.ok) {
|
|
1653
|
-
continue;
|
|
1654
|
-
}
|
|
1655
|
-
const blob = await response.blob();
|
|
1656
|
-
if (blob.size <= PICTURE_MAX_BYTES) {
|
|
1657
|
-
return blob;
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
return;
|
|
1661
|
-
}
|
|
1662
|
-
function parseUploadResult(body) {
|
|
1663
|
-
try {
|
|
1664
|
-
const data = JSON.parse(body);
|
|
1665
|
-
return {
|
|
1666
|
-
key: data.result?.key,
|
|
1667
|
-
message: data.result?.message,
|
|
1668
|
-
success: data.result?.success === true
|
|
1669
|
-
};
|
|
1670
|
-
} catch {
|
|
1671
|
-
return { message: body.slice(0, 300), success: false };
|
|
1672
|
-
}
|
|
1673
|
-
}
|
|
1674
|
-
function throwMixcloudError(status, body) {
|
|
1675
|
-
if (body.includes("An invalid access token was provided")) {
|
|
1676
|
-
throw new CliError2("mixcloud_invalid_token", "Mixcloud rejected the access token. Re-auth with `fluncle admin auth mixcloud`.");
|
|
1677
|
-
}
|
|
1678
|
-
throw new CliError2("mixcloud_request_failed", `Mixcloud responded ${status}${body ? `: ${body.slice(0, 300)}` : ""}`);
|
|
1679
|
-
}
|
|
1680
|
-
var MIXCLOUD_API = "https://api.mixcloud.com", DESCRIPTION_MAX = 1000, PICTURE_MAX_BYTES, COVER_BASE = "https://www.fluncle.com/api/mixtape-cover";
|
|
1681
|
-
var init_mixtape_mixcloud = __esm(() => {
|
|
1682
|
-
init_api();
|
|
1683
|
-
init_output();
|
|
1684
|
-
init_mixtapes();
|
|
1685
|
-
PICTURE_MAX_BYTES = 10 * 1024 * 1024;
|
|
1686
|
-
});
|
|
1687
|
-
|
|
1688
|
-
// src/commands/mixtapes.ts
|
|
1689
|
-
var exports_mixtapes2 = {};
|
|
1690
|
-
__export(exports_mixtapes2, {
|
|
1691
|
-
mixtapesCommand: () => mixtapesCommand2,
|
|
1692
|
-
mixtapeUpdateCommand: () => mixtapeUpdateCommand2,
|
|
1693
|
-
mixtapePublishCommand: () => mixtapePublishCommand2,
|
|
1694
|
-
mixtapeMembersCommand: () => mixtapeMembersCommand2,
|
|
1695
|
-
mixtapeListCommand: () => mixtapeListCommand2,
|
|
1696
|
-
mixtapeGetCommand: () => mixtapeGetCommand2,
|
|
1697
|
-
mixtapeDistributeCommand: () => mixtapeDistributeCommand2,
|
|
1698
|
-
mixtapeDeleteCommand: () => mixtapeDeleteCommand2,
|
|
1699
|
-
mixtapeCreateCommand: () => mixtapeCreateCommand2
|
|
1700
|
-
});
|
|
1701
|
-
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
1702
|
-
async function mixtapesCommand2() {
|
|
1703
|
-
const response = await publicApiGet("/api/mixtapes");
|
|
1704
|
-
return response.mixtapes;
|
|
1705
|
-
}
|
|
1706
|
-
async function mixtapeCreateCommand2(options) {
|
|
1707
|
-
return adminApiPost("/api/admin/mixtapes", buildBody2(options));
|
|
1708
|
-
}
|
|
1709
|
-
async function mixtapeUpdateCommand2(id, options) {
|
|
1710
|
-
return adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(id)}`, buildBody2(options));
|
|
1711
|
-
}
|
|
1712
|
-
async function mixtapeMembersCommand2(id, refs, options) {
|
|
1713
|
-
const members = refs.map((ref) => ({ ref }));
|
|
1714
|
-
if (options.from) {
|
|
1715
|
-
members.push(...parseCueFile2(options.from));
|
|
1716
|
-
}
|
|
1717
|
-
return adminApiPut(`/api/admin/mixtapes/${encodeURIComponent(id)}/members`, { members });
|
|
1718
|
-
}
|
|
1719
|
-
async function mixtapePublishCommand2(id) {
|
|
1720
|
-
return adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(id)}/publish`);
|
|
1721
|
-
}
|
|
1722
|
-
async function mixtapeDeleteCommand2(id) {
|
|
1723
|
-
return adminApiDelete(`/api/admin/mixtapes/${encodeURIComponent(id)}`);
|
|
1724
|
-
}
|
|
1725
|
-
async function mixtapeListCommand2() {
|
|
1726
|
-
const response = await adminApiGet("/api/admin/mixtapes");
|
|
1727
|
-
return response.mixtapes;
|
|
1728
|
-
}
|
|
1729
|
-
async function mixtapeGetCommand2(idOrLogId) {
|
|
1730
|
-
const mixtapes = await mixtapeListCommand2();
|
|
1731
|
-
const match = mixtapes.find((mixtape) => mixtape.id === idOrLogId || mixtape.logId === idOrLogId);
|
|
1732
|
-
if (!match) {
|
|
1733
|
-
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
1734
|
-
}
|
|
1735
|
-
return match;
|
|
1736
|
-
}
|
|
1737
|
-
async function mixtapeDistributeCommand2(idOrLogId, options, onProgress = () => {}) {
|
|
1738
|
-
const mixtape = await mixtapeGetCommand2(idOrLogId);
|
|
1521
|
+
async function mixtapeDeleteCommand(id) {
|
|
1522
|
+
return adminApiDelete(`/api/admin/mixtapes/${encodeURIComponent(id)}`);
|
|
1523
|
+
}
|
|
1524
|
+
async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {}) {
|
|
1525
|
+
const mixtape = await mixtapeGetCommand(idOrLogId);
|
|
1739
1526
|
if (!mixtape.id) {
|
|
1740
1527
|
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
1741
1528
|
}
|
|
@@ -1756,15 +1543,15 @@ async function mixtapeDistributeCommand2(idOrLogId, options, onProgress = () =>
|
|
|
1756
1543
|
let logId = mixtape.logId;
|
|
1757
1544
|
if (!mixtape.durationMs) {
|
|
1758
1545
|
const source = options.audio ?? options.video;
|
|
1759
|
-
const durationMs = source ? await
|
|
1546
|
+
const durationMs = source ? await probeDurationMs(source) : undefined;
|
|
1760
1547
|
if (durationMs) {
|
|
1761
|
-
await
|
|
1548
|
+
await mixtapeUpdateCommand(mixtapeId, { durationMs: String(durationMs), json: false });
|
|
1762
1549
|
onProgress(`Duration: ${Math.round(durationMs / 60000)} min (from the upload).`);
|
|
1763
1550
|
}
|
|
1764
1551
|
}
|
|
1765
1552
|
if (mixtape.status === "draft") {
|
|
1766
1553
|
onProgress("Minting the coordinate…");
|
|
1767
|
-
const published = await
|
|
1554
|
+
const published = await mixtapePublishCommand(mixtapeId);
|
|
1768
1555
|
logId = published.mixtape.logId;
|
|
1769
1556
|
onProgress(`Minted ${logId}.`);
|
|
1770
1557
|
} else if (mixtape.status === "published") {
|
|
@@ -1808,7 +1595,7 @@ async function mixtapeDistributeCommand2(idOrLogId, options, onProgress = () =>
|
|
|
1808
1595
|
}
|
|
1809
1596
|
return { logId, mixtapeId, results };
|
|
1810
1597
|
}
|
|
1811
|
-
async function
|
|
1598
|
+
async function probeDurationMs(filePath) {
|
|
1812
1599
|
try {
|
|
1813
1600
|
const proc = Bun.spawn(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", filePath], { stderr: "ignore", stdout: "pipe" });
|
|
1814
1601
|
const out = await new Response(proc.stdout).text();
|
|
@@ -1819,7 +1606,7 @@ async function probeDurationMs2(filePath) {
|
|
|
1819
1606
|
return;
|
|
1820
1607
|
}
|
|
1821
1608
|
}
|
|
1822
|
-
function
|
|
1609
|
+
function buildBody(options) {
|
|
1823
1610
|
const body = {};
|
|
1824
1611
|
if (options.note !== undefined) {
|
|
1825
1612
|
body.note = options.note;
|
|
@@ -1839,11 +1626,11 @@ function buildBody2(options) {
|
|
|
1839
1626
|
}
|
|
1840
1627
|
return body;
|
|
1841
1628
|
}
|
|
1842
|
-
function
|
|
1843
|
-
if (!
|
|
1629
|
+
function parseCueFile(filePath) {
|
|
1630
|
+
if (!existsSync2(filePath)) {
|
|
1844
1631
|
throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
|
|
1845
1632
|
}
|
|
1846
|
-
const text =
|
|
1633
|
+
const text = readFileSync2(filePath, "utf-8");
|
|
1847
1634
|
const trimmed = text.trim();
|
|
1848
1635
|
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
1849
1636
|
try {
|
|
@@ -1872,9 +1659,9 @@ function parseCueFile2(filePath) {
|
|
|
1872
1659
|
throw new CliError2("invalid_cue_json", `Cue JSON parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
1873
1660
|
}
|
|
1874
1661
|
}
|
|
1875
|
-
return
|
|
1662
|
+
return parseCueSheet(text);
|
|
1876
1663
|
}
|
|
1877
|
-
function
|
|
1664
|
+
function parseCueSheet(text) {
|
|
1878
1665
|
const entries = [];
|
|
1879
1666
|
for (const line of text.split(/\r?\n/)) {
|
|
1880
1667
|
const trimmed = line.trim();
|
|
@@ -1898,9 +1685,10 @@ function parseCueSheet2(text) {
|
|
|
1898
1685
|
}
|
|
1899
1686
|
return entries;
|
|
1900
1687
|
}
|
|
1901
|
-
var
|
|
1688
|
+
var init_mixtapes = __esm(() => {
|
|
1902
1689
|
init_util();
|
|
1903
1690
|
init_api();
|
|
1691
|
+
init_mixtape_api();
|
|
1904
1692
|
init_output();
|
|
1905
1693
|
});
|
|
1906
1694
|
|
|
@@ -2593,7 +2381,7 @@ function parseVersion2(version) {
|
|
|
2593
2381
|
var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
|
|
2594
2382
|
var init_version2 = __esm(() => {
|
|
2595
2383
|
init_output();
|
|
2596
|
-
currentVersion2 = "0.
|
|
2384
|
+
currentVersion2 = "0.75.0".trim() ? "0.75.0".trim() : "0.1.0";
|
|
2597
2385
|
});
|
|
2598
2386
|
|
|
2599
2387
|
// ../../packages/registry/src/index.ts
|
|
@@ -3213,7 +3001,7 @@ var init_src = __esm(() => {
|
|
|
3213
3001
|
],
|
|
3214
3002
|
kind: "cron",
|
|
3215
3003
|
name: "cron.healthcheck",
|
|
3216
|
-
operatorNotes: "every 10m. Pure probing, zero LLM tokens. POSTs to the agent-tier record_health op that /status reads.",
|
|
3004
|
+
operatorNotes: "every 10m, run by a rave-02 host systemd timer (docs/agents/hermes/healthcheck-timer/) — decoupled from the Hermes cron gateway so the prober isn't starved by the scheduler it monitors. Pure probing, zero LLM tokens. POSTs to the agent-tier record_health op that /status reads.",
|
|
3217
3005
|
probeConfig: { cadenceMs: 10 * MINUTE_MS, cronName: "fluncle-healthcheck", kind: "cron" },
|
|
3218
3006
|
weights: { status: "hidden" }
|
|
3219
3007
|
},
|
|
@@ -3975,51 +3763,322 @@ var init_mixtape_youtube2 = __esm(() => {
|
|
|
3975
3763
|
init_output();
|
|
3976
3764
|
});
|
|
3977
3765
|
|
|
3766
|
+
// src/commands/mixtapes.ts
|
|
3767
|
+
var exports_mixtapes2 = {};
|
|
3768
|
+
__export(exports_mixtapes2, {
|
|
3769
|
+
mixtapesCommand: () => mixtapesCommand2,
|
|
3770
|
+
mixtapeUpdateCommand: () => mixtapeUpdateCommand2,
|
|
3771
|
+
mixtapePublishCommand: () => mixtapePublishCommand2,
|
|
3772
|
+
mixtapeMembersCommand: () => mixtapeMembersCommand2,
|
|
3773
|
+
mixtapeListCommand: () => mixtapeListCommand,
|
|
3774
|
+
mixtapeGetCommand: () => mixtapeGetCommand,
|
|
3775
|
+
mixtapeDistributeCommand: () => mixtapeDistributeCommand2,
|
|
3776
|
+
mixtapeDeleteCommand: () => mixtapeDeleteCommand2,
|
|
3777
|
+
mixtapeCreateCommand: () => mixtapeCreateCommand2
|
|
3778
|
+
});
|
|
3779
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
3780
|
+
async function mixtapesCommand2() {
|
|
3781
|
+
const response = await publicApiGet("/api/mixtapes");
|
|
3782
|
+
return response.mixtapes;
|
|
3783
|
+
}
|
|
3784
|
+
async function mixtapeCreateCommand2(options) {
|
|
3785
|
+
return adminApiPost("/api/admin/mixtapes", buildBody2(options));
|
|
3786
|
+
}
|
|
3787
|
+
async function mixtapeUpdateCommand2(id, options) {
|
|
3788
|
+
return adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(id)}`, buildBody2(options));
|
|
3789
|
+
}
|
|
3790
|
+
async function mixtapeMembersCommand2(id, refs, options) {
|
|
3791
|
+
const members = refs.map((ref) => ({ ref }));
|
|
3792
|
+
if (options.from) {
|
|
3793
|
+
members.push(...parseCueFile2(options.from));
|
|
3794
|
+
}
|
|
3795
|
+
return adminApiPut(`/api/admin/mixtapes/${encodeURIComponent(id)}/members`, { members });
|
|
3796
|
+
}
|
|
3797
|
+
async function mixtapePublishCommand2(id) {
|
|
3798
|
+
return adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(id)}/publish`);
|
|
3799
|
+
}
|
|
3800
|
+
async function mixtapeDeleteCommand2(id) {
|
|
3801
|
+
return adminApiDelete(`/api/admin/mixtapes/${encodeURIComponent(id)}`);
|
|
3802
|
+
}
|
|
3803
|
+
async function mixtapeDistributeCommand2(idOrLogId, options, onProgress = () => {}) {
|
|
3804
|
+
const mixtape = await mixtapeGetCommand(idOrLogId);
|
|
3805
|
+
if (!mixtape.id) {
|
|
3806
|
+
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
3807
|
+
}
|
|
3808
|
+
const both = !options.youtube && !options.mixcloud && !options.setVideo;
|
|
3809
|
+
const doYoutube = both || Boolean(options.youtube);
|
|
3810
|
+
const doMixcloud = both || Boolean(options.mixcloud);
|
|
3811
|
+
const doSetVideo = Boolean(options.setVideo);
|
|
3812
|
+
if (doYoutube && !options.video) {
|
|
3813
|
+
throw new CliError2("missing_video", "YouTube distribution needs --video <mp4>");
|
|
3814
|
+
}
|
|
3815
|
+
if (doMixcloud && !options.audio) {
|
|
3816
|
+
throw new CliError2("missing_audio", "Mixcloud distribution needs --audio <file>");
|
|
3817
|
+
}
|
|
3818
|
+
if (doSetVideo && !options.video) {
|
|
3819
|
+
throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
|
|
3820
|
+
}
|
|
3821
|
+
const mixtapeId = mixtape.id;
|
|
3822
|
+
let logId = mixtape.logId;
|
|
3823
|
+
if (!mixtape.durationMs) {
|
|
3824
|
+
const source = options.audio ?? options.video;
|
|
3825
|
+
const durationMs = source ? await probeDurationMs2(source) : undefined;
|
|
3826
|
+
if (durationMs) {
|
|
3827
|
+
await mixtapeUpdateCommand2(mixtapeId, { durationMs: String(durationMs), json: false });
|
|
3828
|
+
onProgress(`Duration: ${Math.round(durationMs / 60000)} min (from the upload).`);
|
|
3829
|
+
}
|
|
3830
|
+
}
|
|
3831
|
+
if (mixtape.status === "draft") {
|
|
3832
|
+
onProgress("Minting the coordinate…");
|
|
3833
|
+
const published = await mixtapePublishCommand2(mixtapeId);
|
|
3834
|
+
logId = published.mixtape.logId;
|
|
3835
|
+
onProgress(`Minted ${logId}.`);
|
|
3836
|
+
} else if (mixtape.status === "published") {
|
|
3837
|
+
onProgress(`Already published (${logId}); re-distributing.`);
|
|
3838
|
+
} else {
|
|
3839
|
+
onProgress(`Resuming distribution for ${logId}.`);
|
|
3840
|
+
}
|
|
3841
|
+
if (!logId) {
|
|
3842
|
+
throw new CliError2("mint_failed", "The mixtape has no Log ID after minting");
|
|
3843
|
+
}
|
|
3844
|
+
const results = [];
|
|
3845
|
+
if (doYoutube) {
|
|
3846
|
+
if (!options.video) {
|
|
3847
|
+
throw new CliError2("missing_video", "YouTube distribution needs --video <mp4>");
|
|
3848
|
+
}
|
|
3849
|
+
onProgress("YouTube: uploading video…");
|
|
3850
|
+
const { distributeYoutube: distributeYoutube3 } = await Promise.resolve().then(() => (init_mixtape_youtube(), exports_mixtape_youtube));
|
|
3851
|
+
const result = await distributeYoutube3(mixtapeId, options.video, onProgress);
|
|
3852
|
+
results.push({ platform: "youtube", url: result.url });
|
|
3853
|
+
onProgress(`YouTube: ${result.url}`);
|
|
3854
|
+
}
|
|
3855
|
+
if (doMixcloud) {
|
|
3856
|
+
if (!options.audio) {
|
|
3857
|
+
throw new CliError2("missing_audio", "Mixcloud distribution needs --audio <file>");
|
|
3858
|
+
}
|
|
3859
|
+
onProgress("Mixcloud: uploading audio…");
|
|
3860
|
+
const { distributeMixcloud: distributeMixcloud2 } = await Promise.resolve().then(() => (init_mixtape_mixcloud(), exports_mixtape_mixcloud));
|
|
3861
|
+
const result = await distributeMixcloud2(mixtapeId, options.audio, onProgress, options.unlisted);
|
|
3862
|
+
results.push({ platform: "mixcloud", url: result.url });
|
|
3863
|
+
onProgress(`Mixcloud: ${result.url}`);
|
|
3864
|
+
}
|
|
3865
|
+
if (doSetVideo) {
|
|
3866
|
+
if (!options.video) {
|
|
3867
|
+
throw new CliError2("missing_video", "--set-video needs --video <master.mp4>");
|
|
3868
|
+
}
|
|
3869
|
+
onProgress("Set video: staging the 1080p rendition…");
|
|
3870
|
+
const { stageSetVideo: stageSetVideo2 } = await Promise.resolve().then(() => (init_mixtape_set_video(), exports_mixtape_set_video));
|
|
3871
|
+
const result = await stageSetVideo2(mixtapeId, options.video, onProgress);
|
|
3872
|
+
results.push({ platform: "set-video", url: result.url });
|
|
3873
|
+
onProgress(`Set video: ${result.url}`);
|
|
3874
|
+
}
|
|
3875
|
+
return { logId, mixtapeId, results };
|
|
3876
|
+
}
|
|
3877
|
+
async function probeDurationMs2(filePath) {
|
|
3878
|
+
try {
|
|
3879
|
+
const proc = Bun.spawn(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", filePath], { stderr: "ignore", stdout: "pipe" });
|
|
3880
|
+
const out = await new Response(proc.stdout).text();
|
|
3881
|
+
await proc.exited;
|
|
3882
|
+
const seconds = Number.parseFloat(out.trim());
|
|
3883
|
+
return Number.isFinite(seconds) && seconds > 0 ? Math.round(seconds * 1000) : undefined;
|
|
3884
|
+
} catch {
|
|
3885
|
+
return;
|
|
3886
|
+
}
|
|
3887
|
+
}
|
|
3888
|
+
function buildBody2(options) {
|
|
3889
|
+
const body = {};
|
|
3890
|
+
if (options.note !== undefined) {
|
|
3891
|
+
body.note = options.note;
|
|
3892
|
+
}
|
|
3893
|
+
if (options.recordedAt !== undefined) {
|
|
3894
|
+
body.recordedAt = options.recordedAt;
|
|
3895
|
+
}
|
|
3896
|
+
if (options.soundcloudUrl !== undefined) {
|
|
3897
|
+
body.soundcloudUrl = options.soundcloudUrl;
|
|
3898
|
+
}
|
|
3899
|
+
if (options.durationMs !== undefined) {
|
|
3900
|
+
const parsed = parseDuration(options.durationMs);
|
|
3901
|
+
if (parsed === null) {
|
|
3902
|
+
throw new CliError2("invalid_duration", "Duration must be mm:ss or h:mm:ss, or a millisecond count");
|
|
3903
|
+
}
|
|
3904
|
+
body.durationMs = parsed;
|
|
3905
|
+
}
|
|
3906
|
+
return body;
|
|
3907
|
+
}
|
|
3908
|
+
function parseCueFile2(filePath) {
|
|
3909
|
+
if (!existsSync3(filePath)) {
|
|
3910
|
+
throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
|
|
3911
|
+
}
|
|
3912
|
+
const text = readFileSync3(filePath, "utf-8");
|
|
3913
|
+
const trimmed = text.trim();
|
|
3914
|
+
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
3915
|
+
try {
|
|
3916
|
+
const parsed = JSON.parse(trimmed);
|
|
3917
|
+
if (!Array.isArray(parsed)) {
|
|
3918
|
+
throw new Error("not an array");
|
|
3919
|
+
}
|
|
3920
|
+
return parsed.map((entry, index) => {
|
|
3921
|
+
if (typeof entry === "string") {
|
|
3922
|
+
return { ref: entry.trim() };
|
|
3923
|
+
}
|
|
3924
|
+
const obj = entry;
|
|
3925
|
+
if (typeof obj?.ref !== "string") {
|
|
3926
|
+
throw new CliError2("invalid_cue_json", `Entry ${index + 1} missing "ref" string`);
|
|
3927
|
+
}
|
|
3928
|
+
const cue = { ref: obj.ref.trim() };
|
|
3929
|
+
if (typeof obj.startMs === "number" && Number.isInteger(obj.startMs) && obj.startMs >= 0) {
|
|
3930
|
+
cue.startMs = obj.startMs;
|
|
3931
|
+
}
|
|
3932
|
+
return cue;
|
|
3933
|
+
});
|
|
3934
|
+
} catch (error) {
|
|
3935
|
+
if (error instanceof CliError2) {
|
|
3936
|
+
throw error;
|
|
3937
|
+
}
|
|
3938
|
+
throw new CliError2("invalid_cue_json", `Cue JSON parse failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3939
|
+
}
|
|
3940
|
+
}
|
|
3941
|
+
return parseCueSheet2(text);
|
|
3942
|
+
}
|
|
3943
|
+
function parseCueSheet2(text) {
|
|
3944
|
+
const entries = [];
|
|
3945
|
+
for (const line of text.split(/\r?\n/)) {
|
|
3946
|
+
const trimmed = line.trim();
|
|
3947
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
3948
|
+
continue;
|
|
3949
|
+
}
|
|
3950
|
+
const match = trimmed.match(/^(\d{1,2}:\d{2}(?::\d{2})?)\s+(.+)$/);
|
|
3951
|
+
if (match) {
|
|
3952
|
+
const [, time, ref] = match;
|
|
3953
|
+
if (time === undefined || ref === undefined) {
|
|
3954
|
+
continue;
|
|
3955
|
+
}
|
|
3956
|
+
const startMs = parseDuration(time);
|
|
3957
|
+
if (startMs === null) {
|
|
3958
|
+
continue;
|
|
3959
|
+
}
|
|
3960
|
+
entries.push({ ref: ref.trim(), startMs });
|
|
3961
|
+
} else {
|
|
3962
|
+
entries.push({ ref: trimmed });
|
|
3963
|
+
}
|
|
3964
|
+
}
|
|
3965
|
+
return entries;
|
|
3966
|
+
}
|
|
3967
|
+
var init_mixtapes2 = __esm(() => {
|
|
3968
|
+
init_util();
|
|
3969
|
+
init_api();
|
|
3970
|
+
init_mixtape_api();
|
|
3971
|
+
init_output();
|
|
3972
|
+
});
|
|
3973
|
+
|
|
3978
3974
|
// src/commands/clips.ts
|
|
3979
3975
|
var exports_clips = {};
|
|
3980
3976
|
__export(exports_clips, {
|
|
3981
3977
|
setVideoUrl: () => setVideoUrl,
|
|
3978
|
+
resolveClipSansFontFile: () => resolveClipSansFontFile,
|
|
3979
|
+
resolveClipFontFile: () => resolveClipFontFile,
|
|
3982
3980
|
escapeDrawtextValue: () => escapeDrawtextValue,
|
|
3983
3981
|
clipsListCommand: () => clipsListCommand,
|
|
3984
3982
|
clipFootageKey: () => clipFootageKey,
|
|
3985
|
-
|
|
3983
|
+
clipCutFilterComplex: () => clipCutFilterComplex,
|
|
3986
3984
|
clipCutFfmpegArgs: () => clipCutFfmpegArgs,
|
|
3987
3985
|
clipCutCommand: () => clipCutCommand,
|
|
3986
|
+
brandDrawtext: () => brandDrawtext,
|
|
3988
3987
|
MAX_CLIP_BYTES: () => MAX_CLIP_BYTES,
|
|
3989
3988
|
CLIP_WIDTH: () => CLIP_WIDTH,
|
|
3989
|
+
CLIP_TITLE_SIZE: () => CLIP_TITLE_SIZE,
|
|
3990
|
+
CLIP_TITLE_COLOR: () => CLIP_TITLE_COLOR,
|
|
3991
|
+
CLIP_SHARP_BORDERW: () => CLIP_SHARP_BORDERW,
|
|
3992
|
+
CLIP_SAFE_BOTTOM: () => CLIP_SAFE_BOTTOM,
|
|
3990
3993
|
CLIP_MAXRATE: () => CLIP_MAXRATE,
|
|
3994
|
+
CLIP_MARGIN_X: () => CLIP_MARGIN_X,
|
|
3995
|
+
CLIP_LINE_GAP: () => CLIP_LINE_GAP,
|
|
3991
3996
|
CLIP_HEIGHT: () => CLIP_HEIGHT,
|
|
3997
|
+
CLIP_HALO_FEATHER_SIGMA: () => CLIP_HALO_FEATHER_SIGMA,
|
|
3998
|
+
CLIP_HALO_CORE_SIGMA: () => CLIP_HALO_CORE_SIGMA,
|
|
3999
|
+
CLIP_HALO_COLOR: () => CLIP_HALO_COLOR,
|
|
3992
4000
|
CLIP_CRF: () => CLIP_CRF,
|
|
4001
|
+
CLIP_COORDINATE_SIZE: () => CLIP_COORDINATE_SIZE,
|
|
4002
|
+
CLIP_COORDINATE_COLOR: () => CLIP_COORDINATE_COLOR,
|
|
3993
4003
|
CLIP_BUFSIZE: () => CLIP_BUFSIZE,
|
|
3994
|
-
CLIP_AUDIO_BITRATE: () => CLIP_AUDIO_BITRATE
|
|
4004
|
+
CLIP_AUDIO_BITRATE: () => CLIP_AUDIO_BITRATE,
|
|
4005
|
+
BOX_CLIP_SANS_FONT_FILE: () => BOX_CLIP_SANS_FONT_FILE,
|
|
4006
|
+
BOX_CLIP_FONT_FILE: () => BOX_CLIP_FONT_FILE
|
|
3995
4007
|
});
|
|
3996
4008
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3997
|
-
import { rmSync as rmSync3, statSync as statSync4 } from "node:fs";
|
|
4009
|
+
import { existsSync as existsSync4, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
|
|
3998
4010
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
3999
4011
|
import { join as join5 } from "node:path";
|
|
4000
4012
|
function clipFootageKey(clipId) {
|
|
4001
4013
|
return `${clipId}/footage.mp4`;
|
|
4002
4014
|
}
|
|
4015
|
+
function resolveFontFile(envVar, bakedPath) {
|
|
4016
|
+
const explicit = process.env[envVar];
|
|
4017
|
+
if (explicit) {
|
|
4018
|
+
return explicit;
|
|
4019
|
+
}
|
|
4020
|
+
return existsSync4(bakedPath) ? bakedPath : undefined;
|
|
4021
|
+
}
|
|
4022
|
+
function resolveClipFontFile() {
|
|
4023
|
+
return resolveFontFile("CLIP_FONT_FILE", BOX_CLIP_FONT_FILE);
|
|
4024
|
+
}
|
|
4025
|
+
function resolveClipSansFontFile() {
|
|
4026
|
+
return resolveFontFile("CLIP_SANS_FONT_FILE", BOX_CLIP_SANS_FONT_FILE);
|
|
4027
|
+
}
|
|
4003
4028
|
function setVideoUrl(logId) {
|
|
4004
4029
|
return `${FOUND_BASE3}/${encodeURIComponent(logId)}/set.mp4`;
|
|
4005
4030
|
}
|
|
4006
4031
|
function escapeDrawtextValue(text) {
|
|
4007
4032
|
return text.replace(/\\/g, "\\\\").replace(/:/g, "\\:").replace(/,/g, "\\,").replace(/%/g, "\\%").replace(/'/g, "'\\''");
|
|
4008
4033
|
}
|
|
4009
|
-
function
|
|
4034
|
+
function brandDrawtext(options) {
|
|
4035
|
+
const value = escapeDrawtextValue(options.text);
|
|
4036
|
+
const font = options.fontFile ? `:fontfile='${options.fontFile}'` : "";
|
|
4037
|
+
const border = options.borderw && options.borderw > 0 ? `:borderw=${options.borderw}:bordercolor=${CLIP_HALO_COLOR}` : "";
|
|
4038
|
+
return `drawtext=text='${value}'${font}:` + `fontcolor=${options.color}:fontsize=${options.size}:` + `x=${options.x}:y=${options.y}${border}`;
|
|
4039
|
+
}
|
|
4040
|
+
function clipCutFilterComplex(options) {
|
|
4010
4041
|
const xOffset = Math.max(0, Math.round(options.xOffset));
|
|
4011
4042
|
const crop = `crop=ih*9/16:ih:${xOffset}:0,scale=${CLIP_WIDTH}:${CLIP_HEIGHT}`;
|
|
4012
|
-
const
|
|
4013
|
-
const
|
|
4014
|
-
const
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4043
|
+
const coordinateBox = Math.round(CLIP_COORDINATE_SIZE * 1.18);
|
|
4044
|
+
const titleBottomOffset = CLIP_SAFE_BOTTOM + coordinateBox + CLIP_LINE_GAP;
|
|
4045
|
+
const title = {
|
|
4046
|
+
fontFile: options.sansFontFile,
|
|
4047
|
+
size: CLIP_TITLE_SIZE,
|
|
4048
|
+
text: options.title,
|
|
4049
|
+
x: CLIP_MARGIN_X,
|
|
4050
|
+
y: `h-${titleBottomOffset}-th`
|
|
4051
|
+
};
|
|
4052
|
+
const coordinate2 = {
|
|
4053
|
+
fontFile: options.oxaniumFontFile,
|
|
4054
|
+
size: CLIP_COORDINATE_SIZE,
|
|
4055
|
+
text: `fluncle://${options.logId}`,
|
|
4056
|
+
x: CLIP_MARGIN_X,
|
|
4057
|
+
y: `h-${CLIP_SAFE_BOTTOM}-th`
|
|
4058
|
+
};
|
|
4059
|
+
const haloGlyphs = [
|
|
4060
|
+
brandDrawtext({ ...title, color: CLIP_HALO_COLOR }),
|
|
4061
|
+
brandDrawtext({ ...coordinate2, color: CLIP_HALO_COLOR })
|
|
4062
|
+
].join(",");
|
|
4063
|
+
const sharpGlyphs = [
|
|
4064
|
+
brandDrawtext({ ...title, borderw: CLIP_SHARP_BORDERW, color: CLIP_TITLE_COLOR }),
|
|
4065
|
+
brandDrawtext({ ...coordinate2, borderw: CLIP_SHARP_BORDERW, color: CLIP_COORDINATE_COLOR })
|
|
4066
|
+
].join(",");
|
|
4067
|
+
return [
|
|
4068
|
+
`[0:v]${crop},setsar=1[base]`,
|
|
4069
|
+
`color=c=black@0:s=${CLIP_WIDTH}x${CLIP_HEIGHT},format=rgba,${haloGlyphs}[ink]`,
|
|
4070
|
+
`[ink]split[ink1][ink2]`,
|
|
4071
|
+
`[ink1]gblur=sigma=${CLIP_HALO_CORE_SIGMA}[core]`,
|
|
4072
|
+
`[ink2]gblur=sigma=${CLIP_HALO_FEATHER_SIGMA}[feather]`,
|
|
4073
|
+
`[base][feather]overlay=0:0[b1]`,
|
|
4074
|
+
`[b1][core]overlay=0:0[b2]`,
|
|
4075
|
+
`[b2]${sharpGlyphs}[out]`
|
|
4076
|
+
].join(";");
|
|
4018
4077
|
}
|
|
4019
4078
|
function clipCutFfmpegArgs(options) {
|
|
4020
4079
|
const inSeconds = (options.inMs / 1000).toFixed(3);
|
|
4021
4080
|
const durationSeconds = ((options.outMs - options.inMs) / 1000).toFixed(3);
|
|
4022
|
-
const filter =
|
|
4081
|
+
const filter = clipCutFilterComplex(options);
|
|
4023
4082
|
return [
|
|
4024
4083
|
"-y",
|
|
4025
4084
|
"-ss",
|
|
@@ -4028,8 +4087,12 @@ function clipCutFfmpegArgs(options) {
|
|
|
4028
4087
|
options.setUrl,
|
|
4029
4088
|
"-t",
|
|
4030
4089
|
durationSeconds,
|
|
4031
|
-
"-
|
|
4090
|
+
"-filter_complex",
|
|
4032
4091
|
filter,
|
|
4092
|
+
"-map",
|
|
4093
|
+
"[out]",
|
|
4094
|
+
"-map",
|
|
4095
|
+
"0:a?",
|
|
4033
4096
|
"-c:v",
|
|
4034
4097
|
"libx264",
|
|
4035
4098
|
"-preset",
|
|
@@ -4069,8 +4132,8 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4069
4132
|
if (!clip) {
|
|
4070
4133
|
throw new CliError2("clip_not_found", `No clip with id ${clipId}`);
|
|
4071
4134
|
}
|
|
4072
|
-
const { mixtapeGetCommand:
|
|
4073
|
-
const mixtape = await
|
|
4135
|
+
const { mixtapeGetCommand: mixtapeGetCommand2 } = await Promise.resolve().then(() => (init_mixtapes2(), exports_mixtapes2));
|
|
4136
|
+
const mixtape = await mixtapeGetCommand2(clip.mixtapeId);
|
|
4074
4137
|
if (!mixtape.logId) {
|
|
4075
4138
|
throw new CliError2("mixtape_no_log_id", `Mixtape ${clip.mixtapeId} has no committed Log ID`);
|
|
4076
4139
|
}
|
|
@@ -4083,11 +4146,12 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4083
4146
|
try {
|
|
4084
4147
|
onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms] from ${mixtape.logId}…`);
|
|
4085
4148
|
await runClipCut({
|
|
4086
|
-
fontFile: process.env.CLIP_FONT_FILE,
|
|
4087
4149
|
inMs: clip.inMs,
|
|
4088
4150
|
logId: mixtape.logId,
|
|
4089
4151
|
outMs: clip.outMs,
|
|
4090
4152
|
outputPath,
|
|
4153
|
+
oxaniumFontFile: resolveClipFontFile(),
|
|
4154
|
+
sansFontFile: resolveClipSansFontFile(),
|
|
4091
4155
|
setUrl,
|
|
4092
4156
|
title: mixtape.title,
|
|
4093
4157
|
xOffset: clip.xOffset
|
|
@@ -4148,7 +4212,7 @@ async function runClipCut(options) {
|
|
|
4148
4212
|
throw new CliError2("ffmpeg_failed", `ffmpeg failed to cut the clip${detail ? `: ${detail}` : ""}`);
|
|
4149
4213
|
}
|
|
4150
4214
|
}
|
|
4151
|
-
var FOUND_BASE3 = "https://found.fluncle.com", CLIP_WIDTH = 1080, CLIP_HEIGHT = 1920, CLIP_CRF = 21, CLIP_MAXRATE = "10M", CLIP_BUFSIZE = "20M", CLIP_AUDIO_BITRATE = "192k", MAX_CLIP_BYTES;
|
|
4215
|
+
var FOUND_BASE3 = "https://found.fluncle.com", CLIP_WIDTH = 1080, CLIP_HEIGHT = 1920, CLIP_CRF = 21, CLIP_MAXRATE = "10M", CLIP_BUFSIZE = "20M", CLIP_AUDIO_BITRATE = "192k", MAX_CLIP_BYTES, CLIP_TITLE_COLOR = "0xf4ead7", CLIP_COORDINATE_COLOR = "0xb7ab95", CLIP_HALO_COLOR = "0x090a0b", CLIP_HALO_CORE_SIGMA = 3, CLIP_HALO_FEATHER_SIGMA = 9, CLIP_SHARP_BORDERW = 1, CLIP_MARGIN_X = 96, CLIP_SAFE_BOTTOM = 230, CLIP_LINE_GAP = 10, CLIP_TITLE_SIZE = 40, CLIP_COORDINATE_SIZE = 22, BOX_CLIP_FONT_FILE = "/opt/fonts/Oxanium-SemiBold.ttf", BOX_CLIP_SANS_FONT_FILE = "/usr/share/fonts/truetype/dejavu/DejaVuSans-Bold.ttf";
|
|
4152
4216
|
var init_clips = __esm(() => {
|
|
4153
4217
|
init_api();
|
|
4154
4218
|
init_output();
|
|
@@ -4164,7 +4228,7 @@ __export(exports_newsletter, {
|
|
|
4164
4228
|
newsletterDraftCommand: () => newsletterDraftCommand,
|
|
4165
4229
|
newsletterDeleteCommand: () => newsletterDeleteCommand
|
|
4166
4230
|
});
|
|
4167
|
-
import { existsSync as
|
|
4231
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4 } from "node:fs";
|
|
4168
4232
|
function buildBody3(options, { requireContent }) {
|
|
4169
4233
|
const body = {};
|
|
4170
4234
|
if (options.contentFile !== undefined) {
|
|
@@ -4184,7 +4248,7 @@ function buildBody3(options, { requireContent }) {
|
|
|
4184
4248
|
return body;
|
|
4185
4249
|
}
|
|
4186
4250
|
function readContentFile(filePath) {
|
|
4187
|
-
if (!
|
|
4251
|
+
if (!existsSync5(filePath)) {
|
|
4188
4252
|
throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
|
|
4189
4253
|
}
|
|
4190
4254
|
const text = readFileSync4(filePath, "utf-8");
|
|
@@ -4511,7 +4575,7 @@ var MIXCLOUD_API2 = "https://api.mixcloud.com", DESCRIPTION_MAX2 = 1000, PICTURE
|
|
|
4511
4575
|
var init_mixtape_mixcloud2 = __esm(() => {
|
|
4512
4576
|
init_api();
|
|
4513
4577
|
init_output();
|
|
4514
|
-
|
|
4578
|
+
init_mixtape_api();
|
|
4515
4579
|
PICTURE_MAX_BYTES2 = 10 * 1024 * 1024;
|
|
4516
4580
|
});
|
|
4517
4581
|
|
|
@@ -4826,7 +4890,7 @@ var init_format2 = __esm(() => {
|
|
|
4826
4890
|
});
|
|
4827
4891
|
|
|
4828
4892
|
// src/cli.ts
|
|
4829
|
-
import { existsSync as
|
|
4893
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
4830
4894
|
import path2 from "path";
|
|
4831
4895
|
|
|
4832
4896
|
// ../../node_modules/commander/lib/error.js
|
|
@@ -6992,7 +7056,7 @@ function addListenCommands(program2) {
|
|
|
6992
7056
|
await runRecent(options, recentCommand3);
|
|
6993
7057
|
});
|
|
6994
7058
|
program2.command("mixtapes").description("Fluncle's checkpoint sets").option("--json", "Print JSON", false).action(async (options) => {
|
|
6995
|
-
const { mixtapesCommand: mixtapesCommand3 } = await Promise.resolve().then(() => (
|
|
7059
|
+
const { mixtapesCommand: mixtapesCommand3 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
6996
7060
|
await runMixtapes(options, mixtapesCommand3);
|
|
6997
7061
|
});
|
|
6998
7062
|
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) => {
|
|
@@ -7046,7 +7110,7 @@ function addMetaCommands(program2) {
|
|
|
7046
7110
|
});
|
|
7047
7111
|
}
|
|
7048
7112
|
function addTrackCommands(program2) {
|
|
7049
|
-
const tracks = configureCommand(program2.command("tracks", { hidden: true }).
|
|
7113
|
+
const tracks = configureCommand(program2.command("tracks", { hidden: true }).description("Public track lookups"));
|
|
7050
7114
|
tracks.command("get").description("Look up one finding by id or Log ID").argument("[idOrLogId]").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
|
|
7051
7115
|
const { trackGetCommand: trackGetCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
|
|
7052
7116
|
await runTrackGet(idOrLogId, options, trackGetCommand2);
|
|
@@ -7060,7 +7124,7 @@ function addAdminCommands(program2) {
|
|
|
7060
7124
|
admin.command("help", { hidden: true }).description("display help for command").action(() => {
|
|
7061
7125
|
admin.outputHelp();
|
|
7062
7126
|
});
|
|
7063
|
-
const adminTracks = configureCommand(admin.command("tracks").
|
|
7127
|
+
const adminTracks = configureCommand(admin.command("tracks").description("Track admin commands"));
|
|
7064
7128
|
adminTracks.action(() => {
|
|
7065
7129
|
adminTracks.outputHelp();
|
|
7066
7130
|
});
|
|
@@ -7068,15 +7132,7 @@ function addAdminCommands(program2) {
|
|
|
7068
7132
|
const { addCommand: addCommand3 } = await Promise.resolve().then(() => (init_add(), exports_add));
|
|
7069
7133
|
await runAdd(spotifyUrl, options, addCommand3);
|
|
7070
7134
|
});
|
|
7071
|
-
|
|
7072
|
-
const { addCommand: addCommand3 } = await Promise.resolve().then(() => (init_add(), exports_add));
|
|
7073
|
-
await runAdd(spotifyUrl, options, addCommand3);
|
|
7074
|
-
});
|
|
7075
|
-
adminTracks.command("queue").description("Findings awaiting a video, oldest first (the next to film is first)").option("--limit <limit>", "Number of findings to show", "10").option("--has-context", "No-op: the render queue is always context-gated").option("--has-observation", "Only findings that already have a spoken observation").option("--json", "Print JSON", false).action(async (options) => {
|
|
7076
|
-
const { queueCommand: queueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7077
|
-
await runAdminQueue(options, queueCommand2);
|
|
7078
|
-
});
|
|
7079
|
-
admin.command("queue", { hidden: true }).description("Render queue (alias of `admin tracks queue`)").option("--limit <limit>", "Number of findings to show", "10").option("--has-context", "No-op: the render queue is always context-gated").option("--has-observation", "Only findings that already have a spoken observation").option("--json", "Print JSON", false).action(async (options) => {
|
|
7135
|
+
adminTracks.command("queue").description("Findings awaiting a video, oldest first (the next to film is first)").option("--limit <limit>", "Number of findings to show", "10").option("--has-observation", "Only findings that already have a spoken observation").option("--json", "Print JSON", false).action(async (options) => {
|
|
7080
7136
|
const { queueCommand: queueCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7081
7137
|
await runAdminQueue(options, queueCommand2);
|
|
7082
7138
|
});
|
|
@@ -7093,10 +7149,6 @@ function addAdminCommands(program2) {
|
|
|
7093
7149
|
const { vehiclesCommand: vehiclesCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7094
7150
|
await runAdminVehicles(options, vehiclesCommand2);
|
|
7095
7151
|
});
|
|
7096
|
-
admin.command("vehicles", { hidden: true }).description("Video vehicles (alias of `admin tracks vehicles`)").option("--limit <limit>", "Number of vehicles to show", "10").option("--json", "Print JSON", false).action(async (options) => {
|
|
7097
|
-
const { vehiclesCommand: vehiclesCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7098
|
-
await runAdminVehicles(options, vehiclesCommand2);
|
|
7099
|
-
});
|
|
7100
7152
|
const adminTrack = adminTracks;
|
|
7101
7153
|
adminTrack.command("update").description("Certify a track into the archive").argument("[trackId]").option("--bpm <number>", "Track BPM").option("--features <json>", "Audio feature JSON").option("--json", "Print JSON", false).option("--key <key>", "Musical key").option("--note <text>", "Operator note").option("--status <status>", "Enrichment status").option("--video-url <url>", "Rendered video URL").allowExcessArguments().action(async (trackId, options) => {
|
|
7102
7154
|
const { trackUpdateCommand: trackUpdateCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
|
|
@@ -7163,35 +7215,35 @@ function addAdminCommands(program2) {
|
|
|
7163
7215
|
adminMixtapes.outputHelp();
|
|
7164
7216
|
});
|
|
7165
7217
|
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) => {
|
|
7166
|
-
const { mixtapeCreateCommand: mixtapeCreateCommand3 } = await Promise.resolve().then(() => (
|
|
7218
|
+
const { mixtapeCreateCommand: mixtapeCreateCommand3 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7167
7219
|
await runMixtapeCreate(options, mixtapeCreateCommand3);
|
|
7168
7220
|
});
|
|
7169
7221
|
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) => {
|
|
7170
|
-
const { mixtapeUpdateCommand: mixtapeUpdateCommand3 } = await Promise.resolve().then(() => (
|
|
7222
|
+
const { mixtapeUpdateCommand: mixtapeUpdateCommand3 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7171
7223
|
await runMixtapeUpdate(id, options, mixtapeUpdateCommand3);
|
|
7172
7224
|
});
|
|
7173
7225
|
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) => {
|
|
7174
|
-
const { mixtapeMembersCommand: mixtapeMembersCommand3 } = await Promise.resolve().then(() => (
|
|
7226
|
+
const { mixtapeMembersCommand: mixtapeMembersCommand3 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7175
7227
|
await runMixtapeMembers(id, refs, options, mixtapeMembersCommand3);
|
|
7176
7228
|
});
|
|
7177
7229
|
adminMixtapes.command("publish").description("Publish a mixtape draft").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7178
|
-
const { mixtapePublishCommand: mixtapePublishCommand3 } = await Promise.resolve().then(() => (
|
|
7230
|
+
const { mixtapePublishCommand: mixtapePublishCommand3 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7179
7231
|
await runMixtapePublish(id, options, mixtapePublishCommand3);
|
|
7180
7232
|
});
|
|
7181
7233
|
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) => {
|
|
7182
|
-
const { mixtapeDeleteCommand: mixtapeDeleteCommand3 } = await Promise.resolve().then(() => (
|
|
7234
|
+
const { mixtapeDeleteCommand: mixtapeDeleteCommand3 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7183
7235
|
await runMixtapeDelete(id, options, mixtapeDeleteCommand3);
|
|
7184
7236
|
});
|
|
7185
7237
|
adminMixtapes.command("list").description("List all mixtapes (including drafts)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7186
|
-
const { mixtapeListCommand:
|
|
7187
|
-
await runMixtapeList(options,
|
|
7238
|
+
const { mixtapeListCommand: mixtapeListCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7239
|
+
await runMixtapeList(options, mixtapeListCommand2);
|
|
7188
7240
|
});
|
|
7189
7241
|
adminMixtapes.command("get").description("Show one mixtape by id or log id").argument("[idOrLogId]").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
|
|
7190
|
-
const { mixtapeGetCommand:
|
|
7191
|
-
await runMixtapeGet(idOrLogId, options,
|
|
7242
|
+
const { mixtapeGetCommand: mixtapeGetCommand2 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7243
|
+
await runMixtapeGet(idOrLogId, options, mixtapeGetCommand2);
|
|
7192
7244
|
});
|
|
7193
7245
|
adminMixtapes.command("distribute").description("Mint + push a mixtape to YouTube (video) and Mixcloud (audio)").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("--set-video", "Also stage a 1080p set-video rendition (from --video) to R2 + flip setVideoAt for the /log player").option("--unlisted", "Keep Mixcloud private too (YouTube is always unlisted until publish-youtube)").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
|
|
7194
|
-
const { mixtapeDistributeCommand: mixtapeDistributeCommand3 } = await Promise.resolve().then(() => (
|
|
7246
|
+
const { mixtapeDistributeCommand: mixtapeDistributeCommand3 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7195
7247
|
await runMixtapeDistribute(idOrLogId, options, mixtapeDistributeCommand3);
|
|
7196
7248
|
});
|
|
7197
7249
|
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) => {
|
|
@@ -7277,7 +7329,7 @@ function addAdminCommands(program2) {
|
|
|
7277
7329
|
const { authLastfmCommand: authLastfmCommand2 } = await Promise.resolve().then(() => (init_auth_lastfm(), exports_auth_lastfm));
|
|
7278
7330
|
await authLastfmCommand2(options);
|
|
7279
7331
|
});
|
|
7280
|
-
const backfill = configureCommand(admin.command("backfills").
|
|
7332
|
+
const backfill = configureCommand(admin.command("backfills").description("Backfill operator-only archives"));
|
|
7281
7333
|
backfill.action(() => {
|
|
7282
7334
|
backfill.outputHelp();
|
|
7283
7335
|
});
|
|
@@ -7515,7 +7567,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
|
|
|
7515
7567
|
return;
|
|
7516
7568
|
}
|
|
7517
7569
|
const candidate = path2.join(dir, name);
|
|
7518
|
-
return
|
|
7570
|
+
return existsSync6(candidate) ? candidate : undefined;
|
|
7519
7571
|
};
|
|
7520
7572
|
const resolveFile = (explicit, name) => {
|
|
7521
7573
|
if (explicit) {
|
|
@@ -7771,8 +7823,8 @@ async function runMixtapeDelete(id, options, mixtapeDeleteCommand3) {
|
|
|
7771
7823
|
}
|
|
7772
7824
|
console.log(`Discarded draft ${id}.`);
|
|
7773
7825
|
}
|
|
7774
|
-
async function runMixtapeList(options,
|
|
7775
|
-
const mixtapes = await
|
|
7826
|
+
async function runMixtapeList(options, mixtapeListCommand2) {
|
|
7827
|
+
const mixtapes = await mixtapeListCommand2();
|
|
7776
7828
|
if (options.json) {
|
|
7777
7829
|
printJson({ mixtapes, ok: true });
|
|
7778
7830
|
return;
|
|
@@ -7813,11 +7865,11 @@ async function runClipsCut(clipId, options, clipCutCommand2) {
|
|
|
7813
7865
|
}
|
|
7814
7866
|
console.log(`Cut ${result.clipId} \u2192 ${result.url} (${(result.sizeBytes / 1e6).toFixed(1)} MB).`);
|
|
7815
7867
|
}
|
|
7816
|
-
async function runMixtapeGet(idOrLogId, options,
|
|
7868
|
+
async function runMixtapeGet(idOrLogId, options, mixtapeGetCommand2) {
|
|
7817
7869
|
if (!idOrLogId) {
|
|
7818
7870
|
throw new Error("Missing mixtape id or log id. Usage: fluncle admin mixtapes get <id|logId>");
|
|
7819
7871
|
}
|
|
7820
|
-
const mixtape = await
|
|
7872
|
+
const mixtape = await mixtapeGetCommand2(idOrLogId);
|
|
7821
7873
|
if (options.json) {
|
|
7822
7874
|
printJson(mixtape);
|
|
7823
7875
|
return;
|
|
@@ -7984,7 +8036,6 @@ function parseListLimit(value) {
|
|
|
7984
8036
|
async function runAdminQueue(options, queueCommand2) {
|
|
7985
8037
|
const limit = parseListLimit(options.limit);
|
|
7986
8038
|
const tracks = await queueCommand2(limit, {
|
|
7987
|
-
hasContext: options.hasContext ? true : undefined,
|
|
7988
8039
|
hasObservation: options.hasObservation ? true : undefined
|
|
7989
8040
|
});
|
|
7990
8041
|
if (options.json) {
|
package/package.json
CHANGED