fluncle 0.77.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/fluncle.mjs +427 -144
- package/package.json +1 -1
package/bin/fluncle.mjs
CHANGED
|
@@ -482,6 +482,20 @@ function resolveClipTracks(options) {
|
|
|
482
482
|
startMs: member.startMs
|
|
483
483
|
}));
|
|
484
484
|
}
|
|
485
|
+
function mixcloudSections(members) {
|
|
486
|
+
return members.filter((member) => member.startMs != null).sort((a, b) => a.startMs - b.startMs).map((member) => ({
|
|
487
|
+
artist: member.artists.join(", "),
|
|
488
|
+
song: member.title,
|
|
489
|
+
start_time: Math.floor(member.startMs / 1000)
|
|
490
|
+
}));
|
|
491
|
+
}
|
|
492
|
+
function mixcloudSectionFields(sections) {
|
|
493
|
+
return sections.flatMap((section, index) => [
|
|
494
|
+
[`sections-${index}-artist`, section.artist],
|
|
495
|
+
[`sections-${index}-song`, section.song],
|
|
496
|
+
[`sections-${index}-start_time`, String(section.start_time)]
|
|
497
|
+
]);
|
|
498
|
+
}
|
|
485
499
|
function baseTitleMatches(findingTitle, candidateTitle) {
|
|
486
500
|
const want = new Set(tokenize(stripVersionSuffix(findingTitle)));
|
|
487
501
|
const have = new Set(tokenize(stripVersionSuffix(candidateTitle)));
|
|
@@ -544,7 +558,7 @@ function parseVersion(version) {
|
|
|
544
558
|
var currentVersion;
|
|
545
559
|
var init_version = __esm(() => {
|
|
546
560
|
init_output();
|
|
547
|
-
currentVersion = "0.
|
|
561
|
+
currentVersion = "0.79.0".trim() ? "0.79.0".trim() : "0.1.0";
|
|
548
562
|
});
|
|
549
563
|
|
|
550
564
|
// src/update-notifier.ts
|
|
@@ -1196,9 +1210,6 @@ var exports_mixtape_mixcloud = {};
|
|
|
1196
1210
|
__export(exports_mixtape_mixcloud, {
|
|
1197
1211
|
resyncMixcloud: () => resyncMixcloud,
|
|
1198
1212
|
mixtapeDescription: () => mixtapeDescription,
|
|
1199
|
-
mixcloudSections: () => mixcloudSections,
|
|
1200
|
-
mixcloudSectionFields: () => mixcloudSectionFields,
|
|
1201
|
-
mixcloudEditUrl: () => mixcloudEditUrl,
|
|
1202
1213
|
distributeMixcloud: () => distributeMixcloud,
|
|
1203
1214
|
authMixcloudCommand: () => authMixcloudCommand
|
|
1204
1215
|
});
|
|
@@ -1259,34 +1270,9 @@ async function distributeMixcloud(mixtapeId, audioPath, onProgress, unlisted = f
|
|
|
1259
1270
|
});
|
|
1260
1271
|
return { url };
|
|
1261
1272
|
}
|
|
1262
|
-
async function resyncMixcloud(mixtapeId
|
|
1263
|
-
const
|
|
1264
|
-
|
|
1265
|
-
const sections = mixcloudSections(mixtape.members);
|
|
1266
|
-
if (sections.length === 0) {
|
|
1267
|
-
throw new CliError2("mixcloud_no_cues", "No cued members to sync — mark cues on the mixtape first.");
|
|
1268
|
-
}
|
|
1269
|
-
const social = await adminApiGet(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/social`);
|
|
1270
|
-
const mixcloud = social.posts.find((post) => post.platform === "mixcloud");
|
|
1271
|
-
const key = mixcloud?.externalId;
|
|
1272
|
-
if (!key) {
|
|
1273
|
-
throw new CliError2("mixcloud_not_distributed", "No Mixcloud cloudcast to re-sync — distribute the mixtape first.");
|
|
1274
|
-
}
|
|
1275
|
-
const form = new FormData;
|
|
1276
|
-
for (const [name, value] of mixcloudSectionFields(sections)) {
|
|
1277
|
-
form.append(name, value);
|
|
1278
|
-
}
|
|
1279
|
-
onProgress?.(`Mixcloud: re-syncing ${sections.length} sections (no re-upload)…`);
|
|
1280
|
-
const response = await fetch(`${mixcloudEditUrl(key)}?access_token=${encodeURIComponent(token)}`, { body: form, method: "POST" });
|
|
1281
|
-
const text = await response.text();
|
|
1282
|
-
if (!response.ok) {
|
|
1283
|
-
throwMixcloudError(response.status, text);
|
|
1284
|
-
}
|
|
1285
|
-
const result = parseUploadResult(text);
|
|
1286
|
-
if (!result.success) {
|
|
1287
|
-
throw new CliError2("mixcloud_edit_rejected", `Mixcloud rejected the section edit: ${result.message ?? text.slice(0, 300)}`);
|
|
1288
|
-
}
|
|
1289
|
-
return { url: mixcloud?.url ?? `https://www.mixcloud.com${key}` };
|
|
1273
|
+
async function resyncMixcloud(mixtapeId) {
|
|
1274
|
+
const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/resync`);
|
|
1275
|
+
return { url: response.url };
|
|
1290
1276
|
}
|
|
1291
1277
|
async function authMixcloudCommand() {
|
|
1292
1278
|
const response = await adminApiGet("/api/admin/mixcloud/auth/start");
|
|
@@ -1311,25 +1297,6 @@ ${breadcrumb}` : breadcrumb;
|
|
|
1311
1297
|
|
|
1312
1298
|
${breadcrumb}` : breadcrumb;
|
|
1313
1299
|
}
|
|
1314
|
-
function mixcloudSections(members) {
|
|
1315
|
-
return members.filter((member) => typeof member.startMs === "number").sort((a, b) => a.startMs - b.startMs).map((member) => ({
|
|
1316
|
-
artist: member.artists.join(", "),
|
|
1317
|
-
song: member.title,
|
|
1318
|
-
start_time: Math.floor(member.startMs / 1000)
|
|
1319
|
-
}));
|
|
1320
|
-
}
|
|
1321
|
-
function mixcloudSectionFields(sections) {
|
|
1322
|
-
return sections.flatMap((section, index) => [
|
|
1323
|
-
[`sections-${index}-artist`, section.artist],
|
|
1324
|
-
[`sections-${index}-song`, section.song],
|
|
1325
|
-
[`sections-${index}-start_time`, String(section.start_time)]
|
|
1326
|
-
]);
|
|
1327
|
-
}
|
|
1328
|
-
function mixcloudEditUrl(key) {
|
|
1329
|
-
const withLeading = key.startsWith("/") ? key : `/${key}`;
|
|
1330
|
-
const path2 = withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
|
|
1331
|
-
return `${MIXCLOUD_API}/upload${path2}edit/`;
|
|
1332
|
-
}
|
|
1333
1300
|
function mixtapeTags(_mixtape) {
|
|
1334
1301
|
return ["Drum & Bass", "Fluncle"];
|
|
1335
1302
|
}
|
|
@@ -1374,6 +1341,7 @@ function throwMixcloudError(status, body) {
|
|
|
1374
1341
|
}
|
|
1375
1342
|
var MIXCLOUD_API = "https://api.mixcloud.com", DESCRIPTION_MAX = 1000, PICTURE_MAX_BYTES, COVER_BASE = "https://www.fluncle.com/api/mixtape-cover";
|
|
1376
1343
|
var init_mixtape_mixcloud = __esm(() => {
|
|
1344
|
+
init_util();
|
|
1377
1345
|
init_api();
|
|
1378
1346
|
init_output();
|
|
1379
1347
|
init_mixtape_api();
|
|
@@ -1383,6 +1351,7 @@ var init_mixtape_mixcloud = __esm(() => {
|
|
|
1383
1351
|
// src/commands/mixtape-set-video.ts
|
|
1384
1352
|
var exports_mixtape_set_video = {};
|
|
1385
1353
|
__export(exports_mixtape_set_video, {
|
|
1354
|
+
uploadRenditionMultipart: () => uploadRenditionMultipart,
|
|
1386
1355
|
stageSetVideo: () => stageSetVideo,
|
|
1387
1356
|
renditionFfmpegArgs: () => renditionFfmpegArgs,
|
|
1388
1357
|
planMultipart: () => planMultipart,
|
|
@@ -1449,7 +1418,7 @@ function renditionFfmpegArgs(inputPath, outputPath) {
|
|
|
1449
1418
|
outputPath
|
|
1450
1419
|
];
|
|
1451
1420
|
}
|
|
1452
|
-
async function
|
|
1421
|
+
async function uploadRenditionMultipart(masterPath, presignPath, onProgress = () => {}) {
|
|
1453
1422
|
if (!existsSync(masterPath)) {
|
|
1454
1423
|
throw new CliError2("file_not_found", `Set-video master not found: ${masterPath}`);
|
|
1455
1424
|
}
|
|
@@ -1461,7 +1430,9 @@ async function stageSetVideo(mixtapeId, masterPath, onProgress = () => {}) {
|
|
|
1461
1430
|
const size = statSync2(renditionPath).size;
|
|
1462
1431
|
const plan = planMultipart(size);
|
|
1463
1432
|
onProgress(`Set video: uploading ${(size / 1e9).toFixed(2)} GB in ${plan.partCount} part(s)…`);
|
|
1464
|
-
const presign = await adminApiPost(
|
|
1433
|
+
const presign = await adminApiPost(presignPath, {
|
|
1434
|
+
partCount: plan.partCount
|
|
1435
|
+
});
|
|
1465
1436
|
const urlByPart = new Map(presign.parts.map((part) => [part.partNumber, part.url]));
|
|
1466
1437
|
const completed = [];
|
|
1467
1438
|
try {
|
|
@@ -1479,13 +1450,19 @@ async function stageSetVideo(mixtapeId, masterPath, onProgress = () => {}) {
|
|
|
1479
1450
|
await abortUpload(presign.abortUrl).catch(() => {});
|
|
1480
1451
|
throw error;
|
|
1481
1452
|
}
|
|
1482
|
-
await adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}`, { setVideoAt: new Date().toISOString() });
|
|
1483
|
-
onProgress("Set video: flipped setVideoAt — the /log player + video SEO are live.");
|
|
1484
1453
|
return { key: presign.key, url: `${FOUND_BASE}/${presign.key}` };
|
|
1485
1454
|
} finally {
|
|
1486
1455
|
rmSync2(renditionPath, { force: true });
|
|
1487
1456
|
}
|
|
1488
1457
|
}
|
|
1458
|
+
async function stageSetVideo(mixtapeId, masterPath, onProgress = () => {}) {
|
|
1459
|
+
const result = await uploadRenditionMultipart(masterPath, `/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/set-video/presign`, onProgress);
|
|
1460
|
+
await adminApiPatch(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}`, {
|
|
1461
|
+
setVideoAt: new Date().toISOString()
|
|
1462
|
+
});
|
|
1463
|
+
onProgress("Set video: flipped setVideoAt — the /log player + video SEO are live.");
|
|
1464
|
+
return result;
|
|
1465
|
+
}
|
|
1489
1466
|
async function putPart(url, path2, part) {
|
|
1490
1467
|
const response = await fetch(url, {
|
|
1491
1468
|
body: Bun.file(path2).slice(part.start, part.end),
|
|
@@ -1695,7 +1672,7 @@ async function mixtapeResyncCommand(idOrLogId, options, onProgress = () => {}) {
|
|
|
1695
1672
|
if (doMixcloud) {
|
|
1696
1673
|
onProgress("Mixcloud: re-syncing sections…");
|
|
1697
1674
|
const { resyncMixcloud: resyncMixcloud2 } = await Promise.resolve().then(() => (init_mixtape_mixcloud(), exports_mixtape_mixcloud));
|
|
1698
|
-
const result = await resyncMixcloud2(mixtapeId
|
|
1675
|
+
const result = await resyncMixcloud2(mixtapeId);
|
|
1699
1676
|
results.push({ platform: "mixcloud", url: result.url });
|
|
1700
1677
|
onProgress(`Mixcloud: ${result.url}`);
|
|
1701
1678
|
}
|
|
@@ -2487,7 +2464,7 @@ function parseVersion2(version) {
|
|
|
2487
2464
|
var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
|
|
2488
2465
|
var init_version2 = __esm(() => {
|
|
2489
2466
|
init_output();
|
|
2490
|
-
currentVersion2 = "0.
|
|
2467
|
+
currentVersion2 = "0.79.0".trim() ? "0.79.0".trim() : "0.1.0";
|
|
2491
2468
|
});
|
|
2492
2469
|
|
|
2493
2470
|
// ../../packages/registry/src/index.ts
|
|
@@ -3874,6 +3851,145 @@ var init_mixtape_youtube2 = __esm(() => {
|
|
|
3874
3851
|
init_output();
|
|
3875
3852
|
});
|
|
3876
3853
|
|
|
3854
|
+
// src/commands/recordings.ts
|
|
3855
|
+
var exports_recordings = {};
|
|
3856
|
+
__export(exports_recordings, {
|
|
3857
|
+
recordingsListCommand: () => recordingsListCommand,
|
|
3858
|
+
recordingUpdateCommand: () => recordingUpdateCommand,
|
|
3859
|
+
recordingPromoteCommand: () => recordingPromoteCommand,
|
|
3860
|
+
recordingGetCommand: () => recordingGetCommand,
|
|
3861
|
+
recordingGet: () => recordingGet,
|
|
3862
|
+
recordingDeleteCommand: () => recordingDeleteCommand,
|
|
3863
|
+
recordingCreateCommand: () => recordingCreateCommand
|
|
3864
|
+
});
|
|
3865
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "node:fs";
|
|
3866
|
+
async function recordingGet(id) {
|
|
3867
|
+
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
3868
|
+
return response.recording;
|
|
3869
|
+
}
|
|
3870
|
+
function formatRecordingSummary(recording) {
|
|
3871
|
+
const promoted = recording.logId ? ` → fluncle://${recording.logId}` : " (un-promoted)";
|
|
3872
|
+
const cues = recording.tracklist.length;
|
|
3873
|
+
return `${recording.id} ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
|
|
3874
|
+
}
|
|
3875
|
+
async function recordingsListCommand(options = {}) {
|
|
3876
|
+
const response = await adminApiGet("/api/admin/recordings");
|
|
3877
|
+
if (options.json) {
|
|
3878
|
+
printJson2({ ok: true, recordings: response.recordings });
|
|
3879
|
+
return;
|
|
3880
|
+
}
|
|
3881
|
+
if (response.recordings.length === 0) {
|
|
3882
|
+
console.log("No recordings yet.");
|
|
3883
|
+
return;
|
|
3884
|
+
}
|
|
3885
|
+
console.log(response.recordings.map(formatRecordingSummary).join(`
|
|
3886
|
+
`));
|
|
3887
|
+
}
|
|
3888
|
+
async function recordingGetCommand(id, options = {}) {
|
|
3889
|
+
if (!id) {
|
|
3890
|
+
throw new CliError2("missing_id", "Missing recording id for: get");
|
|
3891
|
+
}
|
|
3892
|
+
const recording = await recordingGet(id);
|
|
3893
|
+
if (options.json) {
|
|
3894
|
+
printJson2({ ok: true, recording });
|
|
3895
|
+
return;
|
|
3896
|
+
}
|
|
3897
|
+
console.log(formatRecordingSummary(recording));
|
|
3898
|
+
console.log(` r2Key: ${recording.r2Key}`);
|
|
3899
|
+
for (const cue of recording.tracklist) {
|
|
3900
|
+
const at = cue.startMs === undefined ? "—" : `${(cue.startMs / 1000).toFixed(1)}s`;
|
|
3901
|
+
console.log(` ${at} ${cue.artists.join(", ")} — ${cue.title}`);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
async function recordingCreateCommand(options = {}) {
|
|
3905
|
+
const title = options.title?.trim();
|
|
3906
|
+
if (!title) {
|
|
3907
|
+
throw new CliError2("missing_title", "A recording needs a --title");
|
|
3908
|
+
}
|
|
3909
|
+
if (!options.video) {
|
|
3910
|
+
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
3911
|
+
}
|
|
3912
|
+
if (!existsSync3(options.video)) {
|
|
3913
|
+
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
3914
|
+
}
|
|
3915
|
+
const created = await adminApiPost("/api/admin/recordings", {
|
|
3916
|
+
recordedAt: options.recordedAt,
|
|
3917
|
+
title
|
|
3918
|
+
});
|
|
3919
|
+
const recording = created.recording;
|
|
3920
|
+
const log = (message) => {
|
|
3921
|
+
if (!options.json) {
|
|
3922
|
+
console.log(message);
|
|
3923
|
+
}
|
|
3924
|
+
};
|
|
3925
|
+
log(`Recording ${recording.id} created — staging the set video…`);
|
|
3926
|
+
const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
|
|
3927
|
+
if (options.json) {
|
|
3928
|
+
printJson2({ key: upload.key, ok: true, recording, url: upload.url });
|
|
3929
|
+
return;
|
|
3930
|
+
}
|
|
3931
|
+
console.log(`Recording ${recording.id} staged → ${upload.url}`);
|
|
3932
|
+
}
|
|
3933
|
+
async function recordingUpdateCommand(id, options = {}) {
|
|
3934
|
+
if (!id) {
|
|
3935
|
+
throw new CliError2("missing_id", "Missing recording id for: update");
|
|
3936
|
+
}
|
|
3937
|
+
const body = {};
|
|
3938
|
+
if (options.title !== undefined) {
|
|
3939
|
+
body.title = options.title;
|
|
3940
|
+
}
|
|
3941
|
+
if (options.recordedAt !== undefined) {
|
|
3942
|
+
body.recordedAt = options.recordedAt;
|
|
3943
|
+
}
|
|
3944
|
+
if (options.tracklistFile !== undefined) {
|
|
3945
|
+
if (!existsSync3(options.tracklistFile)) {
|
|
3946
|
+
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
3947
|
+
}
|
|
3948
|
+
try {
|
|
3949
|
+
body.tracklistJson = JSON.parse(readFileSync3(options.tracklistFile, "utf8"));
|
|
3950
|
+
} catch {
|
|
3951
|
+
throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
|
|
3952
|
+
}
|
|
3953
|
+
}
|
|
3954
|
+
if (Object.keys(body).length === 0) {
|
|
3955
|
+
throw new CliError2("no_fields", "Nothing to update — pass --title, --recorded-at, or --tracklist-file");
|
|
3956
|
+
}
|
|
3957
|
+
const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
|
|
3958
|
+
if (options.json) {
|
|
3959
|
+
printJson2({ ok: true, recording: response.recording });
|
|
3960
|
+
return;
|
|
3961
|
+
}
|
|
3962
|
+
console.log(`Updated ${formatRecordingSummary(response.recording)}`);
|
|
3963
|
+
}
|
|
3964
|
+
async function recordingDeleteCommand(id, options = {}) {
|
|
3965
|
+
if (!id) {
|
|
3966
|
+
throw new CliError2("missing_id", "Missing recording id for: delete");
|
|
3967
|
+
}
|
|
3968
|
+
await adminApiDelete(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
3969
|
+
if (options.json) {
|
|
3970
|
+
printJson2({ ok: true });
|
|
3971
|
+
return;
|
|
3972
|
+
}
|
|
3973
|
+
console.log(`Deleted recording ${id} (and its clips).`);
|
|
3974
|
+
}
|
|
3975
|
+
async function recordingPromoteCommand(id, options = {}) {
|
|
3976
|
+
if (!id) {
|
|
3977
|
+
throw new CliError2("missing_id", "Missing recording id for: promote");
|
|
3978
|
+
}
|
|
3979
|
+
const response = await adminApiPost(`/api/admin/recordings/${encodeURIComponent(id)}/promote`);
|
|
3980
|
+
const recording = response.recording;
|
|
3981
|
+
if (options.json) {
|
|
3982
|
+
printJson2({ ok: true, recording });
|
|
3983
|
+
return;
|
|
3984
|
+
}
|
|
3985
|
+
console.log(recording.logId ? `Promoted recording ${id} → mixtape fluncle://${recording.logId}` : `Promoted recording ${id}`);
|
|
3986
|
+
}
|
|
3987
|
+
var init_recordings = __esm(() => {
|
|
3988
|
+
init_api();
|
|
3989
|
+
init_output();
|
|
3990
|
+
init_mixtape_set_video();
|
|
3991
|
+
});
|
|
3992
|
+
|
|
3877
3993
|
// src/commands/mixtapes.ts
|
|
3878
3994
|
var exports_mixtapes2 = {};
|
|
3879
3995
|
__export(exports_mixtapes2, {
|
|
@@ -3888,7 +4004,7 @@ __export(exports_mixtapes2, {
|
|
|
3888
4004
|
mixtapeDeleteCommand: () => mixtapeDeleteCommand2,
|
|
3889
4005
|
mixtapeCreateCommand: () => mixtapeCreateCommand2
|
|
3890
4006
|
});
|
|
3891
|
-
import { existsSync as
|
|
4007
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "node:fs";
|
|
3892
4008
|
async function mixtapesCommand2() {
|
|
3893
4009
|
const response = await publicApiGet("/api/mixtapes");
|
|
3894
4010
|
return response.mixtapes;
|
|
@@ -4018,7 +4134,7 @@ async function mixtapeResyncCommand2(idOrLogId, options, onProgress = () => {})
|
|
|
4018
4134
|
if (doMixcloud) {
|
|
4019
4135
|
onProgress("Mixcloud: re-syncing sections…");
|
|
4020
4136
|
const { resyncMixcloud: resyncMixcloud2 } = await Promise.resolve().then(() => (init_mixtape_mixcloud(), exports_mixtape_mixcloud));
|
|
4021
|
-
const result = await resyncMixcloud2(mixtapeId
|
|
4137
|
+
const result = await resyncMixcloud2(mixtapeId);
|
|
4022
4138
|
results.push({ platform: "mixcloud", url: result.url });
|
|
4023
4139
|
onProgress(`Mixcloud: ${result.url}`);
|
|
4024
4140
|
}
|
|
@@ -4056,10 +4172,10 @@ function buildBody2(options) {
|
|
|
4056
4172
|
return body;
|
|
4057
4173
|
}
|
|
4058
4174
|
function parseCueFile2(filePath) {
|
|
4059
|
-
if (!
|
|
4175
|
+
if (!existsSync4(filePath)) {
|
|
4060
4176
|
throw new CliError2("file_not_found", `Cue file not found: ${filePath}`);
|
|
4061
4177
|
}
|
|
4062
|
-
const text =
|
|
4178
|
+
const text = readFileSync4(filePath, "utf-8");
|
|
4063
4179
|
const trimmed = text.trim();
|
|
4064
4180
|
if (trimmed.startsWith("[") || trimmed.startsWith("{")) {
|
|
4065
4181
|
try {
|
|
@@ -4156,7 +4272,7 @@ __export(exports_clips, {
|
|
|
4156
4272
|
BOX_CLIP_FONT_FILE: () => BOX_CLIP_FONT_FILE
|
|
4157
4273
|
});
|
|
4158
4274
|
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
4159
|
-
import { existsSync as
|
|
4275
|
+
import { existsSync as existsSync5, rmSync as rmSync3, statSync as statSync4 } from "node:fs";
|
|
4160
4276
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
4161
4277
|
import { join as join5 } from "node:path";
|
|
4162
4278
|
function clipFootageKey(clipId) {
|
|
@@ -4167,7 +4283,7 @@ function resolveFontFile(envVar, bakedPath) {
|
|
|
4167
4283
|
if (explicit) {
|
|
4168
4284
|
return explicit;
|
|
4169
4285
|
}
|
|
4170
|
-
return
|
|
4286
|
+
return existsSync5(bakedPath) ? bakedPath : undefined;
|
|
4171
4287
|
}
|
|
4172
4288
|
function resolveClipFontFile() {
|
|
4173
4289
|
return resolveFontFile("CLIP_FONT_FILE", BOX_CLIP_FONT_FILE);
|
|
@@ -4217,24 +4333,32 @@ function resolveTitleLines(options, y) {
|
|
|
4217
4333
|
function clipCutFilterComplex(options) {
|
|
4218
4334
|
const xOffset = Math.max(0, Math.round(options.xOffset));
|
|
4219
4335
|
const crop = `crop=ih*9/16:ih:${xOffset}:0,scale=${CLIP_WIDTH}:${CLIP_HEIGHT}`;
|
|
4336
|
+
const logId = options.logId?.trim();
|
|
4337
|
+
const hasCoordinate = Boolean(logId);
|
|
4220
4338
|
const coordinateBox = Math.round(CLIP_COORDINATE_SIZE * 1.18);
|
|
4221
|
-
const titleBottomOffset = CLIP_SAFE_BOTTOM + coordinateBox + CLIP_LINE_GAP;
|
|
4339
|
+
const titleBottomOffset = hasCoordinate ? CLIP_SAFE_BOTTOM + coordinateBox + CLIP_LINE_GAP : CLIP_SAFE_BOTTOM;
|
|
4222
4340
|
const titleY = `h-${titleBottomOffset}-th`;
|
|
4223
4341
|
const titleLines = resolveTitleLines(options, titleY);
|
|
4224
|
-
const coordinate2 = {
|
|
4342
|
+
const coordinate2 = hasCoordinate ? {
|
|
4225
4343
|
fontFile: options.oxaniumFontFile,
|
|
4226
4344
|
size: CLIP_COORDINATE_SIZE,
|
|
4227
|
-
text: `fluncle://${
|
|
4345
|
+
text: `fluncle://${logId}`,
|
|
4228
4346
|
x: CLIP_MARGIN_X,
|
|
4229
4347
|
y: `h-${CLIP_SAFE_BOTTOM}-th`
|
|
4230
|
-
};
|
|
4348
|
+
} : undefined;
|
|
4231
4349
|
const haloGlyphs = [
|
|
4232
4350
|
...titleLines.map((line) => brandDrawtext({ ...line, color: CLIP_HALO_COLOR })),
|
|
4233
|
-
brandDrawtext({ ...coordinate2, color: CLIP_HALO_COLOR })
|
|
4351
|
+
...coordinate2 ? [brandDrawtext({ ...coordinate2, color: CLIP_HALO_COLOR })] : []
|
|
4234
4352
|
].join(",");
|
|
4235
4353
|
const sharpGlyphs = [
|
|
4236
4354
|
...titleLines.map((line) => brandDrawtext({ ...line, borderw: CLIP_SHARP_BORDERW, color: CLIP_TITLE_COLOR })),
|
|
4237
|
-
|
|
4355
|
+
...coordinate2 ? [
|
|
4356
|
+
brandDrawtext({
|
|
4357
|
+
...coordinate2,
|
|
4358
|
+
borderw: CLIP_SHARP_BORDERW,
|
|
4359
|
+
color: CLIP_COORDINATE_COLOR
|
|
4360
|
+
})
|
|
4361
|
+
] : []
|
|
4238
4362
|
].join(",");
|
|
4239
4363
|
return [
|
|
4240
4364
|
`[0:v]${crop},setsar=1[base]`,
|
|
@@ -4288,6 +4412,9 @@ function clipCutFfmpegArgs(options) {
|
|
|
4288
4412
|
}
|
|
4289
4413
|
async function clipsListCommand(filter = {}) {
|
|
4290
4414
|
const params = new URLSearchParams;
|
|
4415
|
+
if (filter.recordingId) {
|
|
4416
|
+
params.set("recordingId", filter.recordingId);
|
|
4417
|
+
}
|
|
4291
4418
|
if (filter.mixtapeId) {
|
|
4292
4419
|
params.set("mixtapeId", filter.mixtapeId);
|
|
4293
4420
|
}
|
|
@@ -4298,11 +4425,27 @@ async function clipsListCommand(filter = {}) {
|
|
|
4298
4425
|
const response = await adminApiGet(`/api/admin/clips${query ? `?${query}` : ""}`);
|
|
4299
4426
|
return response.clips;
|
|
4300
4427
|
}
|
|
4301
|
-
async function
|
|
4302
|
-
|
|
4303
|
-
|
|
4304
|
-
|
|
4305
|
-
|
|
4428
|
+
async function resolveClipSource(clip) {
|
|
4429
|
+
if (clip.recordingId) {
|
|
4430
|
+
const { recordingGet: recordingGet2 } = await Promise.resolve().then(() => (init_recordings(), exports_recordings));
|
|
4431
|
+
const recording = await recordingGet2(clip.recordingId);
|
|
4432
|
+
if (!recording.r2Key) {
|
|
4433
|
+
throw new CliError2("recording_not_staged", `Recording ${clip.recordingId} has no staged set video`);
|
|
4434
|
+
}
|
|
4435
|
+
return {
|
|
4436
|
+
logId: recording.logId,
|
|
4437
|
+
members: recording.tracklist.map((cue) => ({
|
|
4438
|
+
artists: cue.artists,
|
|
4439
|
+
startMs: cue.startMs,
|
|
4440
|
+
title: cue.title
|
|
4441
|
+
})),
|
|
4442
|
+
setDurationMs: recording.durationMs ?? 0,
|
|
4443
|
+
setUrl: `${FOUND_BASE3}/${recording.r2Key.split("/").map(encodeURIComponent).join("/")}`,
|
|
4444
|
+
title: recording.title
|
|
4445
|
+
};
|
|
4446
|
+
}
|
|
4447
|
+
if (!clip.mixtapeId) {
|
|
4448
|
+
throw new CliError2("clip_unlinked", `Clip ${clip.id} is linked to neither a recording nor a mixtape`);
|
|
4306
4449
|
}
|
|
4307
4450
|
const { mixtapeGetCommand: mixtapeGetCommand2 } = await Promise.resolve().then(() => (init_mixtapes2(), exports_mixtapes2));
|
|
4308
4451
|
const mixtape = await mixtapeGetCommand2(clip.mixtapeId);
|
|
@@ -4312,22 +4455,36 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4312
4455
|
if (!mixtape.setVideoAt) {
|
|
4313
4456
|
throw new CliError2("set_not_staged", `Mixtape ${mixtape.logId} has no staged set video — run \`distribute --set-video\` first`);
|
|
4314
4457
|
}
|
|
4458
|
+
return {
|
|
4459
|
+
logId: mixtape.logId,
|
|
4460
|
+
members: mixtape.members,
|
|
4461
|
+
setDurationMs: mixtape.durationMs ?? 0,
|
|
4462
|
+
setUrl: setVideoUrl(mixtape.logId),
|
|
4463
|
+
title: mixtape.title
|
|
4464
|
+
};
|
|
4465
|
+
}
|
|
4466
|
+
async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
4467
|
+
const clips = await clipsListCommand();
|
|
4468
|
+
const clip = clips.find((candidate) => candidate.id === clipId);
|
|
4469
|
+
if (!clip) {
|
|
4470
|
+
throw new CliError2("clip_not_found", `No clip with id ${clipId}`);
|
|
4471
|
+
}
|
|
4472
|
+
const source = await resolveClipSource(clip);
|
|
4315
4473
|
await assertFfmpeg2();
|
|
4316
|
-
const setUrl = setVideoUrl(mixtape.logId);
|
|
4317
4474
|
const outputPath = join5(tmpdir2(), `fluncle-clip-${randomUUID2()}.mp4`);
|
|
4318
4475
|
try {
|
|
4319
|
-
onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms] from ${
|
|
4476
|
+
onProgress(`Clip ${clipId}: cutting [${clip.inMs}–${clip.outMs}ms]${source.logId ? ` from ${source.logId}` : ""}…`);
|
|
4320
4477
|
await runClipCut({
|
|
4321
4478
|
inMs: clip.inMs,
|
|
4322
|
-
logId:
|
|
4323
|
-
members:
|
|
4479
|
+
logId: source.logId,
|
|
4480
|
+
members: source.members,
|
|
4324
4481
|
outMs: clip.outMs,
|
|
4325
4482
|
outputPath,
|
|
4326
4483
|
oxaniumFontFile: resolveClipFontFile(),
|
|
4327
4484
|
sansFontFile: resolveClipSansFontFile(),
|
|
4328
|
-
setDurationMs:
|
|
4329
|
-
setUrl,
|
|
4330
|
-
title:
|
|
4485
|
+
setDurationMs: source.setDurationMs,
|
|
4486
|
+
setUrl: source.setUrl,
|
|
4487
|
+
title: source.title,
|
|
4331
4488
|
xOffset: clip.xOffset
|
|
4332
4489
|
});
|
|
4333
4490
|
const sizeBytes = statSync4(outputPath).size;
|
|
@@ -4342,7 +4499,7 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4342
4499
|
return {
|
|
4343
4500
|
clipId,
|
|
4344
4501
|
key: presign.key,
|
|
4345
|
-
logId:
|
|
4502
|
+
logId: source.logId,
|
|
4346
4503
|
sizeBytes,
|
|
4347
4504
|
url: `${FOUND_BASE3}/${presign.key}`
|
|
4348
4505
|
};
|
|
@@ -4394,6 +4551,145 @@ var init_clips = __esm(() => {
|
|
|
4394
4551
|
MAX_CLIP_BYTES = 100 * 1024 * 1024;
|
|
4395
4552
|
});
|
|
4396
4553
|
|
|
4554
|
+
// src/commands/recordings.ts
|
|
4555
|
+
var exports_recordings2 = {};
|
|
4556
|
+
__export(exports_recordings2, {
|
|
4557
|
+
recordingsListCommand: () => recordingsListCommand2,
|
|
4558
|
+
recordingUpdateCommand: () => recordingUpdateCommand2,
|
|
4559
|
+
recordingPromoteCommand: () => recordingPromoteCommand2,
|
|
4560
|
+
recordingGetCommand: () => recordingGetCommand2,
|
|
4561
|
+
recordingGet: () => recordingGet2,
|
|
4562
|
+
recordingDeleteCommand: () => recordingDeleteCommand2,
|
|
4563
|
+
recordingCreateCommand: () => recordingCreateCommand2
|
|
4564
|
+
});
|
|
4565
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "node:fs";
|
|
4566
|
+
async function recordingGet2(id) {
|
|
4567
|
+
const response = await adminApiGet(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4568
|
+
return response.recording;
|
|
4569
|
+
}
|
|
4570
|
+
function formatRecordingSummary2(recording) {
|
|
4571
|
+
const promoted = recording.logId ? ` → fluncle://${recording.logId}` : " (un-promoted)";
|
|
4572
|
+
const cues = recording.tracklist.length;
|
|
4573
|
+
return `${recording.id} ${recording.title}${promoted} [${cues} cue${cues === 1 ? "" : "s"}]`;
|
|
4574
|
+
}
|
|
4575
|
+
async function recordingsListCommand2(options = {}) {
|
|
4576
|
+
const response = await adminApiGet("/api/admin/recordings");
|
|
4577
|
+
if (options.json) {
|
|
4578
|
+
printJson2({ ok: true, recordings: response.recordings });
|
|
4579
|
+
return;
|
|
4580
|
+
}
|
|
4581
|
+
if (response.recordings.length === 0) {
|
|
4582
|
+
console.log("No recordings yet.");
|
|
4583
|
+
return;
|
|
4584
|
+
}
|
|
4585
|
+
console.log(response.recordings.map(formatRecordingSummary2).join(`
|
|
4586
|
+
`));
|
|
4587
|
+
}
|
|
4588
|
+
async function recordingGetCommand2(id, options = {}) {
|
|
4589
|
+
if (!id) {
|
|
4590
|
+
throw new CliError2("missing_id", "Missing recording id for: get");
|
|
4591
|
+
}
|
|
4592
|
+
const recording = await recordingGet2(id);
|
|
4593
|
+
if (options.json) {
|
|
4594
|
+
printJson2({ ok: true, recording });
|
|
4595
|
+
return;
|
|
4596
|
+
}
|
|
4597
|
+
console.log(formatRecordingSummary2(recording));
|
|
4598
|
+
console.log(` r2Key: ${recording.r2Key}`);
|
|
4599
|
+
for (const cue of recording.tracklist) {
|
|
4600
|
+
const at = cue.startMs === undefined ? "—" : `${(cue.startMs / 1000).toFixed(1)}s`;
|
|
4601
|
+
console.log(` ${at} ${cue.artists.join(", ")} — ${cue.title}`);
|
|
4602
|
+
}
|
|
4603
|
+
}
|
|
4604
|
+
async function recordingCreateCommand2(options = {}) {
|
|
4605
|
+
const title = options.title?.trim();
|
|
4606
|
+
if (!title) {
|
|
4607
|
+
throw new CliError2("missing_title", "A recording needs a --title");
|
|
4608
|
+
}
|
|
4609
|
+
if (!options.video) {
|
|
4610
|
+
throw new CliError2("missing_video", "A recording needs a --video <file> to stage");
|
|
4611
|
+
}
|
|
4612
|
+
if (!existsSync6(options.video)) {
|
|
4613
|
+
throw new CliError2("file_not_found", `Set-video master not found: ${options.video}`);
|
|
4614
|
+
}
|
|
4615
|
+
const created = await adminApiPost("/api/admin/recordings", {
|
|
4616
|
+
recordedAt: options.recordedAt,
|
|
4617
|
+
title
|
|
4618
|
+
});
|
|
4619
|
+
const recording = created.recording;
|
|
4620
|
+
const log = (message) => {
|
|
4621
|
+
if (!options.json) {
|
|
4622
|
+
console.log(message);
|
|
4623
|
+
}
|
|
4624
|
+
};
|
|
4625
|
+
log(`Recording ${recording.id} created — staging the set video…`);
|
|
4626
|
+
const upload = await uploadRenditionMultipart(options.video, `/api/admin/recordings/${encodeURIComponent(recording.id)}/set-video/presign`, log);
|
|
4627
|
+
if (options.json) {
|
|
4628
|
+
printJson2({ key: upload.key, ok: true, recording, url: upload.url });
|
|
4629
|
+
return;
|
|
4630
|
+
}
|
|
4631
|
+
console.log(`Recording ${recording.id} staged → ${upload.url}`);
|
|
4632
|
+
}
|
|
4633
|
+
async function recordingUpdateCommand2(id, options = {}) {
|
|
4634
|
+
if (!id) {
|
|
4635
|
+
throw new CliError2("missing_id", "Missing recording id for: update");
|
|
4636
|
+
}
|
|
4637
|
+
const body = {};
|
|
4638
|
+
if (options.title !== undefined) {
|
|
4639
|
+
body.title = options.title;
|
|
4640
|
+
}
|
|
4641
|
+
if (options.recordedAt !== undefined) {
|
|
4642
|
+
body.recordedAt = options.recordedAt;
|
|
4643
|
+
}
|
|
4644
|
+
if (options.tracklistFile !== undefined) {
|
|
4645
|
+
if (!existsSync6(options.tracklistFile)) {
|
|
4646
|
+
throw new CliError2("file_not_found", `Tracklist file not found: ${options.tracklistFile}`);
|
|
4647
|
+
}
|
|
4648
|
+
try {
|
|
4649
|
+
body.tracklistJson = JSON.parse(readFileSync5(options.tracklistFile, "utf8"));
|
|
4650
|
+
} catch {
|
|
4651
|
+
throw new CliError2("invalid_tracklist", `Tracklist file is not valid JSON: ${options.tracklistFile}`);
|
|
4652
|
+
}
|
|
4653
|
+
}
|
|
4654
|
+
if (Object.keys(body).length === 0) {
|
|
4655
|
+
throw new CliError2("no_fields", "Nothing to update — pass --title, --recorded-at, or --tracklist-file");
|
|
4656
|
+
}
|
|
4657
|
+
const response = await adminApiPatch(`/api/admin/recordings/${encodeURIComponent(id)}`, body);
|
|
4658
|
+
if (options.json) {
|
|
4659
|
+
printJson2({ ok: true, recording: response.recording });
|
|
4660
|
+
return;
|
|
4661
|
+
}
|
|
4662
|
+
console.log(`Updated ${formatRecordingSummary2(response.recording)}`);
|
|
4663
|
+
}
|
|
4664
|
+
async function recordingDeleteCommand2(id, options = {}) {
|
|
4665
|
+
if (!id) {
|
|
4666
|
+
throw new CliError2("missing_id", "Missing recording id for: delete");
|
|
4667
|
+
}
|
|
4668
|
+
await adminApiDelete(`/api/admin/recordings/${encodeURIComponent(id)}`);
|
|
4669
|
+
if (options.json) {
|
|
4670
|
+
printJson2({ ok: true });
|
|
4671
|
+
return;
|
|
4672
|
+
}
|
|
4673
|
+
console.log(`Deleted recording ${id} (and its clips).`);
|
|
4674
|
+
}
|
|
4675
|
+
async function recordingPromoteCommand2(id, options = {}) {
|
|
4676
|
+
if (!id) {
|
|
4677
|
+
throw new CliError2("missing_id", "Missing recording id for: promote");
|
|
4678
|
+
}
|
|
4679
|
+
const response = await adminApiPost(`/api/admin/recordings/${encodeURIComponent(id)}/promote`);
|
|
4680
|
+
const recording = response.recording;
|
|
4681
|
+
if (options.json) {
|
|
4682
|
+
printJson2({ ok: true, recording });
|
|
4683
|
+
return;
|
|
4684
|
+
}
|
|
4685
|
+
console.log(recording.logId ? `Promoted recording ${id} → mixtape fluncle://${recording.logId}` : `Promoted recording ${id}`);
|
|
4686
|
+
}
|
|
4687
|
+
var init_recordings2 = __esm(() => {
|
|
4688
|
+
init_api();
|
|
4689
|
+
init_output();
|
|
4690
|
+
init_mixtape_set_video();
|
|
4691
|
+
});
|
|
4692
|
+
|
|
4397
4693
|
// src/commands/newsletter.ts
|
|
4398
4694
|
var exports_newsletter = {};
|
|
4399
4695
|
__export(exports_newsletter, {
|
|
@@ -4403,7 +4699,7 @@ __export(exports_newsletter, {
|
|
|
4403
4699
|
newsletterDraftCommand: () => newsletterDraftCommand,
|
|
4404
4700
|
newsletterDeleteCommand: () => newsletterDeleteCommand
|
|
4405
4701
|
});
|
|
4406
|
-
import { existsSync as
|
|
4702
|
+
import { existsSync as existsSync7, readFileSync as readFileSync6 } from "node:fs";
|
|
4407
4703
|
function buildBody3(options, { requireContent }) {
|
|
4408
4704
|
const body = {};
|
|
4409
4705
|
if (options.contentFile !== undefined) {
|
|
@@ -4423,10 +4719,10 @@ function buildBody3(options, { requireContent }) {
|
|
|
4423
4719
|
return body;
|
|
4424
4720
|
}
|
|
4425
4721
|
function readContentFile(filePath) {
|
|
4426
|
-
if (!
|
|
4722
|
+
if (!existsSync7(filePath)) {
|
|
4427
4723
|
throw new CliError2("file_not_found", `Content file not found: ${filePath}`);
|
|
4428
4724
|
}
|
|
4429
|
-
const text =
|
|
4725
|
+
const text = readFileSync6(filePath, "utf-8");
|
|
4430
4726
|
try {
|
|
4431
4727
|
return JSON.parse(text);
|
|
4432
4728
|
} catch (error) {
|
|
@@ -4612,9 +4908,6 @@ var exports_mixtape_mixcloud2 = {};
|
|
|
4612
4908
|
__export(exports_mixtape_mixcloud2, {
|
|
4613
4909
|
resyncMixcloud: () => resyncMixcloud2,
|
|
4614
4910
|
mixtapeDescription: () => mixtapeDescription2,
|
|
4615
|
-
mixcloudSections: () => mixcloudSections2,
|
|
4616
|
-
mixcloudSectionFields: () => mixcloudSectionFields2,
|
|
4617
|
-
mixcloudEditUrl: () => mixcloudEditUrl2,
|
|
4618
4911
|
distributeMixcloud: () => distributeMixcloud2,
|
|
4619
4912
|
authMixcloudCommand: () => authMixcloudCommand2
|
|
4620
4913
|
});
|
|
@@ -4641,8 +4934,8 @@ async function distributeMixcloud2(mixtapeId, audioPath, onProgress, unlisted =
|
|
|
4641
4934
|
for (const [index, tag] of mixtapeTags2(mixtape).entries()) {
|
|
4642
4935
|
form.append(`tags-${index}-tag`, tag);
|
|
4643
4936
|
}
|
|
4644
|
-
const sections =
|
|
4645
|
-
for (const [name, value] of
|
|
4937
|
+
const sections = mixcloudSections(mixtape.members);
|
|
4938
|
+
for (const [name, value] of mixcloudSectionFields(sections)) {
|
|
4646
4939
|
form.append(name, value);
|
|
4647
4940
|
}
|
|
4648
4941
|
if (unlisted) {
|
|
@@ -4675,34 +4968,9 @@ async function distributeMixcloud2(mixtapeId, audioPath, onProgress, unlisted =
|
|
|
4675
4968
|
});
|
|
4676
4969
|
return { url };
|
|
4677
4970
|
}
|
|
4678
|
-
async function resyncMixcloud2(mixtapeId
|
|
4679
|
-
const
|
|
4680
|
-
|
|
4681
|
-
const sections = mixcloudSections2(mixtape.members);
|
|
4682
|
-
if (sections.length === 0) {
|
|
4683
|
-
throw new CliError2("mixcloud_no_cues", "No cued members to sync — mark cues on the mixtape first.");
|
|
4684
|
-
}
|
|
4685
|
-
const social = await adminApiGet(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/social`);
|
|
4686
|
-
const mixcloud = social.posts.find((post) => post.platform === "mixcloud");
|
|
4687
|
-
const key = mixcloud?.externalId;
|
|
4688
|
-
if (!key) {
|
|
4689
|
-
throw new CliError2("mixcloud_not_distributed", "No Mixcloud cloudcast to re-sync — distribute the mixtape first.");
|
|
4690
|
-
}
|
|
4691
|
-
const form = new FormData;
|
|
4692
|
-
for (const [name, value] of mixcloudSectionFields2(sections)) {
|
|
4693
|
-
form.append(name, value);
|
|
4694
|
-
}
|
|
4695
|
-
onProgress?.(`Mixcloud: re-syncing ${sections.length} sections (no re-upload)…`);
|
|
4696
|
-
const response = await fetch(`${mixcloudEditUrl2(key)}?access_token=${encodeURIComponent(token)}`, { body: form, method: "POST" });
|
|
4697
|
-
const text = await response.text();
|
|
4698
|
-
if (!response.ok) {
|
|
4699
|
-
throwMixcloudError2(response.status, text);
|
|
4700
|
-
}
|
|
4701
|
-
const result = parseUploadResult2(text);
|
|
4702
|
-
if (!result.success) {
|
|
4703
|
-
throw new CliError2("mixcloud_edit_rejected", `Mixcloud rejected the section edit: ${result.message ?? text.slice(0, 300)}`);
|
|
4704
|
-
}
|
|
4705
|
-
return { url: mixcloud?.url ?? `https://www.mixcloud.com${key}` };
|
|
4971
|
+
async function resyncMixcloud2(mixtapeId) {
|
|
4972
|
+
const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/mixcloud/resync`);
|
|
4973
|
+
return { url: response.url };
|
|
4706
4974
|
}
|
|
4707
4975
|
async function authMixcloudCommand2() {
|
|
4708
4976
|
const response = await adminApiGet("/api/admin/mixcloud/auth/start");
|
|
@@ -4727,25 +4995,6 @@ ${breadcrumb}` : breadcrumb;
|
|
|
4727
4995
|
|
|
4728
4996
|
${breadcrumb}` : breadcrumb;
|
|
4729
4997
|
}
|
|
4730
|
-
function mixcloudSections2(members) {
|
|
4731
|
-
return members.filter((member) => typeof member.startMs === "number").sort((a, b) => a.startMs - b.startMs).map((member) => ({
|
|
4732
|
-
artist: member.artists.join(", "),
|
|
4733
|
-
song: member.title,
|
|
4734
|
-
start_time: Math.floor(member.startMs / 1000)
|
|
4735
|
-
}));
|
|
4736
|
-
}
|
|
4737
|
-
function mixcloudSectionFields2(sections) {
|
|
4738
|
-
return sections.flatMap((section, index) => [
|
|
4739
|
-
[`sections-${index}-artist`, section.artist],
|
|
4740
|
-
[`sections-${index}-song`, section.song],
|
|
4741
|
-
[`sections-${index}-start_time`, String(section.start_time)]
|
|
4742
|
-
]);
|
|
4743
|
-
}
|
|
4744
|
-
function mixcloudEditUrl2(key) {
|
|
4745
|
-
const withLeading = key.startsWith("/") ? key : `/${key}`;
|
|
4746
|
-
const path2 = withLeading.endsWith("/") ? withLeading : `${withLeading}/`;
|
|
4747
|
-
return `${MIXCLOUD_API2}/upload${path2}edit/`;
|
|
4748
|
-
}
|
|
4749
4998
|
function mixtapeTags2(_mixtape) {
|
|
4750
4999
|
return ["Drum & Bass", "Fluncle"];
|
|
4751
5000
|
}
|
|
@@ -4790,6 +5039,7 @@ function throwMixcloudError2(status, body) {
|
|
|
4790
5039
|
}
|
|
4791
5040
|
var MIXCLOUD_API2 = "https://api.mixcloud.com", DESCRIPTION_MAX2 = 1000, PICTURE_MAX_BYTES2, COVER_BASE2 = "https://www.fluncle.com/api/mixtape-cover";
|
|
4792
5041
|
var init_mixtape_mixcloud2 = __esm(() => {
|
|
5042
|
+
init_util();
|
|
4793
5043
|
init_api();
|
|
4794
5044
|
init_output();
|
|
4795
5045
|
init_mixtape_api();
|
|
@@ -5107,7 +5357,7 @@ var init_format2 = __esm(() => {
|
|
|
5107
5357
|
});
|
|
5108
5358
|
|
|
5109
5359
|
// src/cli.ts
|
|
5110
|
-
import { existsSync as
|
|
5360
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7 } from "fs";
|
|
5111
5361
|
import path2 from "path";
|
|
5112
5362
|
|
|
5113
5363
|
// ../../node_modules/commander/lib/error.js
|
|
@@ -7475,7 +7725,7 @@ function addAdminCommands(program2) {
|
|
|
7475
7725
|
adminClips.action(() => {
|
|
7476
7726
|
adminClips.outputHelp();
|
|
7477
7727
|
});
|
|
7478
|
-
adminClips.command("list").description("List clips (filter by --status pending|done and/or --mixtape <id>)").option("--status <status>", "Filter by cut status (pending|done)").option("--mixtape <id>", "Filter by mixtape id").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7728
|
+
adminClips.command("list").description("List clips (filter by --status pending|done, --recording <id>, and/or --mixtape <id>)").option("--status <status>", "Filter by cut status (pending|done)").option("--recording <id>", "Filter by recording id").option("--mixtape <id>", "Filter by mixtape id").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7479
7729
|
const { clipsListCommand: clipsListCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7480
7730
|
await runClipsList(options, clipsListCommand2);
|
|
7481
7731
|
});
|
|
@@ -7483,6 +7733,34 @@ function addAdminCommands(program2) {
|
|
|
7483
7733
|
const { clipCutCommand: clipCutCommand2 } = await Promise.resolve().then(() => (init_clips(), exports_clips));
|
|
7484
7734
|
await runClipsCut(clipId, options, clipCutCommand2);
|
|
7485
7735
|
});
|
|
7736
|
+
const adminRecordings = configureCommand(admin.command("recordings").description("Recording (unpublished set) commands"));
|
|
7737
|
+
adminRecordings.action(() => {
|
|
7738
|
+
adminRecordings.outputHelp();
|
|
7739
|
+
});
|
|
7740
|
+
adminRecordings.command("create").description("Create a recording + stage its set video (from --video)").option("--title <text>", "Recording title").option("--video <file>", "Set-video master to stage (a 1080p rendition is derived)").option("--recorded-at <date>", "Recorded date (ISO)").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7741
|
+
const { recordingCreateCommand: recordingCreateCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7742
|
+
await recordingCreateCommand3(options);
|
|
7743
|
+
});
|
|
7744
|
+
adminRecordings.command("list").description("List every recording").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
7745
|
+
const { recordingsListCommand: recordingsListCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7746
|
+
await recordingsListCommand3(options);
|
|
7747
|
+
});
|
|
7748
|
+
adminRecordings.command("get").description("Show one recording by id").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7749
|
+
const { recordingGetCommand: recordingGetCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7750
|
+
await recordingGetCommand3(id, options);
|
|
7751
|
+
});
|
|
7752
|
+
adminRecordings.command("update").description("Update a recording's title, recorded date, or tracklist (--tracklist-file)").argument("[id]").option("--title <text>", "Recording title").option("--recorded-at <date>", "Recorded date (ISO)").option("--tracklist-file <file>", "JSON file with the whole cue tracklist array").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7753
|
+
const { recordingUpdateCommand: recordingUpdateCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7754
|
+
await recordingUpdateCommand3(id, options);
|
|
7755
|
+
});
|
|
7756
|
+
adminRecordings.command("delete").description("Delete a recording (cascade its clips)").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7757
|
+
const { recordingDeleteCommand: recordingDeleteCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7758
|
+
await recordingDeleteCommand3(id, options);
|
|
7759
|
+
});
|
|
7760
|
+
adminRecordings.command("promote").description("Promote a recording to a published mixtape (mint-or-reuse; idempotent)").argument("[id]").option("--json", "Print JSON", false).allowExcessArguments().action(async (id, options) => {
|
|
7761
|
+
const { recordingPromoteCommand: recordingPromoteCommand3 } = await Promise.resolve().then(() => (init_recordings2(), exports_recordings2));
|
|
7762
|
+
await recordingPromoteCommand3(id, options);
|
|
7763
|
+
});
|
|
7486
7764
|
const adminNewsletter = configureCommand(admin.command("newsletter").description("Newsletter edition commands"));
|
|
7487
7765
|
adminNewsletter.action(() => {
|
|
7488
7766
|
adminNewsletter.outputHelp();
|
|
@@ -7586,7 +7864,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
|
|
|
7586
7864
|
console.log(` mime: ${result.mime}`);
|
|
7587
7865
|
}
|
|
7588
7866
|
async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
|
|
7589
|
-
const script = options.scriptFile ?
|
|
7867
|
+
const script = options.scriptFile ? readFileSync7(options.scriptFile, "utf8") : options.script;
|
|
7590
7868
|
if (!idOrLogId || !script || !script.trim()) {
|
|
7591
7869
|
throw new Error("Usage: fluncle admin tracks observe <track_id|log_id> (--script <text> | --script-file <file>) [--voice-id <id>] [--duration-ms <ms>] [--context-note <text>] [--json]");
|
|
7592
7870
|
}
|
|
@@ -7642,7 +7920,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
|
|
|
7642
7920
|
}
|
|
7643
7921
|
}
|
|
7644
7922
|
async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
|
|
7645
|
-
const note = options.scriptFile ?
|
|
7923
|
+
const note = options.scriptFile ? readFileSync7(options.scriptFile, "utf8") : options.script;
|
|
7646
7924
|
if (!idOrLogId || !note || !note.trim()) {
|
|
7647
7925
|
throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
|
|
7648
7926
|
}
|
|
@@ -7788,7 +8066,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
|
|
|
7788
8066
|
return;
|
|
7789
8067
|
}
|
|
7790
8068
|
const candidate = path2.join(dir, name);
|
|
7791
|
-
return
|
|
8069
|
+
return existsSync8(candidate) ? candidate : undefined;
|
|
7792
8070
|
};
|
|
7793
8071
|
const resolveFile = (explicit, name) => {
|
|
7794
8072
|
if (explicit) {
|
|
@@ -8076,7 +8354,11 @@ async function runMixtapeList(options, mixtapeListCommand2) {
|
|
|
8076
8354
|
}
|
|
8077
8355
|
}
|
|
8078
8356
|
async function runClipsList(options, clipsListCommand2) {
|
|
8079
|
-
const clips = await clipsListCommand2({
|
|
8357
|
+
const clips = await clipsListCommand2({
|
|
8358
|
+
mixtapeId: options.mixtape,
|
|
8359
|
+
recordingId: options.recording,
|
|
8360
|
+
status: options.status
|
|
8361
|
+
});
|
|
8080
8362
|
if (options.json) {
|
|
8081
8363
|
printJson({ clips, ok: true });
|
|
8082
8364
|
return;
|
|
@@ -8086,7 +8368,8 @@ async function runClipsList(options, clipsListCommand2) {
|
|
|
8086
8368
|
return;
|
|
8087
8369
|
}
|
|
8088
8370
|
for (const clip of clips) {
|
|
8089
|
-
|
|
8371
|
+
const source = clip.recordingId ?? clip.mixtapeId ?? "\u2014";
|
|
8372
|
+
console.log(`${clip.id} ${clip.status} ${source} ${clip.inMs}-${clip.outMs}ms x=${clip.xOffset}`);
|
|
8090
8373
|
}
|
|
8091
8374
|
}
|
|
8092
8375
|
async function runClipsCut(clipId, options, clipCutCommand2) {
|
package/package.json
CHANGED