fluncle 0.75.0 → 0.77.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 +256 -20
- package/package.json +1 -1
package/bin/fluncle.mjs
CHANGED
|
@@ -462,6 +462,26 @@ function versionMatches(findingTitle, candidateTitle) {
|
|
|
462
462
|
}
|
|
463
463
|
return !candidateIsRemix;
|
|
464
464
|
}
|
|
465
|
+
function trackLabel(artists, title) {
|
|
466
|
+
const joined = artists.join(", ");
|
|
467
|
+
return joined && title ? `${joined} — ${title}` : joined || title;
|
|
468
|
+
}
|
|
469
|
+
function resolveClipTracks(options) {
|
|
470
|
+
const { inMs, members, outMs, setDurationMs } = options;
|
|
471
|
+
const cued = members.filter((member) => member.startMs != null).sort((a, b) => a.startMs - b.startMs);
|
|
472
|
+
if (cued.length === 0) {
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
const lastIndex = cued.length - 1;
|
|
476
|
+
return cued.filter((member, index) => {
|
|
477
|
+
const lo = index === 0 ? Math.min(member.startMs, inMs) : member.startMs;
|
|
478
|
+
const hi = index === lastIndex ? Math.max(setDurationMs, outMs) : cued[index + 1]?.startMs ?? Number.POSITIVE_INFINITY;
|
|
479
|
+
return lo < outMs && inMs < hi;
|
|
480
|
+
}).map((member) => ({
|
|
481
|
+
label: trackLabel(member.artists, member.title),
|
|
482
|
+
startMs: member.startMs
|
|
483
|
+
}));
|
|
484
|
+
}
|
|
465
485
|
function baseTitleMatches(findingTitle, candidateTitle) {
|
|
466
486
|
const want = new Set(tokenize(stripVersionSuffix(findingTitle)));
|
|
467
487
|
const have = new Set(tokenize(stripVersionSuffix(candidateTitle)));
|
|
@@ -524,7 +544,7 @@ function parseVersion(version) {
|
|
|
524
544
|
var currentVersion;
|
|
525
545
|
var init_version = __esm(() => {
|
|
526
546
|
init_output();
|
|
527
|
-
currentVersion = "0.
|
|
547
|
+
currentVersion = "0.77.0".trim() ? "0.77.0".trim() : "0.1.0";
|
|
528
548
|
});
|
|
529
549
|
|
|
530
550
|
// src/update-notifier.ts
|
|
@@ -1009,6 +1029,7 @@ var init_mixtape_api = __esm(() => {
|
|
|
1009
1029
|
// src/commands/mixtape-youtube.ts
|
|
1010
1030
|
var exports_mixtape_youtube = {};
|
|
1011
1031
|
__export(exports_mixtape_youtube, {
|
|
1032
|
+
resyncYoutube: () => resyncYoutube,
|
|
1012
1033
|
publishYoutubeCommand: () => publishYoutubeCommand,
|
|
1013
1034
|
nextOffset: () => nextOffset,
|
|
1014
1035
|
distributeYoutube: () => distributeYoutube,
|
|
@@ -1064,6 +1085,10 @@ async function publishYoutubeCommand(idOrLogId) {
|
|
|
1064
1085
|
const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/publish`);
|
|
1065
1086
|
return { url: response.url };
|
|
1066
1087
|
}
|
|
1088
|
+
async function resyncYoutube(mixtapeId) {
|
|
1089
|
+
const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/resync`);
|
|
1090
|
+
return { url: response.url, videoId: response.videoId };
|
|
1091
|
+
}
|
|
1067
1092
|
async function authYoutubeCommand() {
|
|
1068
1093
|
const response = await adminApiGet("/api/admin/youtube/auth/start");
|
|
1069
1094
|
console.log(`Open this YouTube authorization URL:
|
|
@@ -1169,8 +1194,11 @@ var init_mixtape_youtube = __esm(() => {
|
|
|
1169
1194
|
// src/commands/mixtape-mixcloud.ts
|
|
1170
1195
|
var exports_mixtape_mixcloud = {};
|
|
1171
1196
|
__export(exports_mixtape_mixcloud, {
|
|
1197
|
+
resyncMixcloud: () => resyncMixcloud,
|
|
1172
1198
|
mixtapeDescription: () => mixtapeDescription,
|
|
1173
1199
|
mixcloudSections: () => mixcloudSections,
|
|
1200
|
+
mixcloudSectionFields: () => mixcloudSectionFields,
|
|
1201
|
+
mixcloudEditUrl: () => mixcloudEditUrl,
|
|
1174
1202
|
distributeMixcloud: () => distributeMixcloud,
|
|
1175
1203
|
authMixcloudCommand: () => authMixcloudCommand
|
|
1176
1204
|
});
|
|
@@ -1198,10 +1226,8 @@ async function distributeMixcloud(mixtapeId, audioPath, onProgress, unlisted = f
|
|
|
1198
1226
|
form.append(`tags-${index}-tag`, tag);
|
|
1199
1227
|
}
|
|
1200
1228
|
const sections = mixcloudSections(mixtape.members);
|
|
1201
|
-
for (const [
|
|
1202
|
-
form.append(
|
|
1203
|
-
form.append(`sections-${index}-song`, section.song);
|
|
1204
|
-
form.append(`sections-${index}-start_time`, String(section.start_time));
|
|
1229
|
+
for (const [name, value] of mixcloudSectionFields(sections)) {
|
|
1230
|
+
form.append(name, value);
|
|
1205
1231
|
}
|
|
1206
1232
|
if (unlisted) {
|
|
1207
1233
|
form.append("unlisted", "1");
|
|
@@ -1233,6 +1259,35 @@ async function distributeMixcloud(mixtapeId, audioPath, onProgress, unlisted = f
|
|
|
1233
1259
|
});
|
|
1234
1260
|
return { url };
|
|
1235
1261
|
}
|
|
1262
|
+
async function resyncMixcloud(mixtapeId, onProgress) {
|
|
1263
|
+
const token = await fetchMixcloudToken();
|
|
1264
|
+
const mixtape = await mixtapeGetCommand(mixtapeId);
|
|
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}` };
|
|
1290
|
+
}
|
|
1236
1291
|
async function authMixcloudCommand() {
|
|
1237
1292
|
const response = await adminApiGet("/api/admin/mixcloud/auth/start");
|
|
1238
1293
|
console.log(`Open this Mixcloud authorization URL:
|
|
@@ -1263,6 +1318,18 @@ function mixcloudSections(members) {
|
|
|
1263
1318
|
start_time: Math.floor(member.startMs / 1000)
|
|
1264
1319
|
}));
|
|
1265
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
|
+
}
|
|
1266
1333
|
function mixtapeTags(_mixtape) {
|
|
1267
1334
|
return ["Drum & Bass", "Fluncle"];
|
|
1268
1335
|
}
|
|
@@ -1489,6 +1556,7 @@ var exports_mixtapes = {};
|
|
|
1489
1556
|
__export(exports_mixtapes, {
|
|
1490
1557
|
mixtapesCommand: () => mixtapesCommand,
|
|
1491
1558
|
mixtapeUpdateCommand: () => mixtapeUpdateCommand,
|
|
1559
|
+
mixtapeResyncCommand: () => mixtapeResyncCommand,
|
|
1492
1560
|
mixtapePublishCommand: () => mixtapePublishCommand,
|
|
1493
1561
|
mixtapeMembersCommand: () => mixtapeMembersCommand,
|
|
1494
1562
|
mixtapeListCommand: () => mixtapeListCommand,
|
|
@@ -1595,6 +1663,44 @@ async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {
|
|
|
1595
1663
|
}
|
|
1596
1664
|
return { logId, mixtapeId, results };
|
|
1597
1665
|
}
|
|
1666
|
+
async function mixtapeResyncCommand(idOrLogId, options, onProgress = () => {}) {
|
|
1667
|
+
const mixtape = await mixtapeGetCommand(idOrLogId);
|
|
1668
|
+
if (!mixtape.id) {
|
|
1669
|
+
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
1670
|
+
}
|
|
1671
|
+
if (!mixtape.logId) {
|
|
1672
|
+
throw new CliError2("mixtape_no_log_id", "The mixtape isn't published yet — distribute it before re-syncing.");
|
|
1673
|
+
}
|
|
1674
|
+
const mixtapeId = mixtape.id;
|
|
1675
|
+
const explicit = Boolean(options.youtube) || Boolean(options.mixcloud);
|
|
1676
|
+
let doYoutube = Boolean(options.youtube);
|
|
1677
|
+
let doMixcloud = Boolean(options.mixcloud);
|
|
1678
|
+
if (!explicit) {
|
|
1679
|
+
const social = await adminApiGet(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/social`);
|
|
1680
|
+
const platforms = new Set(social.posts.map((post) => post.platform));
|
|
1681
|
+
doYoutube = platforms.has("youtube");
|
|
1682
|
+
doMixcloud = platforms.has("mixcloud");
|
|
1683
|
+
if (!doYoutube && !doMixcloud) {
|
|
1684
|
+
throw new CliError2("mixtape_not_distributed", "The mixtape has no YouTube or Mixcloud link to re-sync.");
|
|
1685
|
+
}
|
|
1686
|
+
}
|
|
1687
|
+
const results = [];
|
|
1688
|
+
if (doYoutube) {
|
|
1689
|
+
onProgress("YouTube: re-syncing description + chapters…");
|
|
1690
|
+
const { resyncYoutube: resyncYoutube2 } = await Promise.resolve().then(() => (init_mixtape_youtube(), exports_mixtape_youtube));
|
|
1691
|
+
const result = await resyncYoutube2(mixtapeId);
|
|
1692
|
+
results.push({ platform: "youtube", url: result.url });
|
|
1693
|
+
onProgress(`YouTube: ${result.url}`);
|
|
1694
|
+
}
|
|
1695
|
+
if (doMixcloud) {
|
|
1696
|
+
onProgress("Mixcloud: re-syncing sections…");
|
|
1697
|
+
const { resyncMixcloud: resyncMixcloud2 } = await Promise.resolve().then(() => (init_mixtape_mixcloud(), exports_mixtape_mixcloud));
|
|
1698
|
+
const result = await resyncMixcloud2(mixtapeId, onProgress);
|
|
1699
|
+
results.push({ platform: "mixcloud", url: result.url });
|
|
1700
|
+
onProgress(`Mixcloud: ${result.url}`);
|
|
1701
|
+
}
|
|
1702
|
+
return { logId: mixtape.logId, mixtapeId, results };
|
|
1703
|
+
}
|
|
1598
1704
|
async function probeDurationMs(filePath) {
|
|
1599
1705
|
try {
|
|
1600
1706
|
const proc = Bun.spawn(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", filePath], { stderr: "ignore", stdout: "pipe" });
|
|
@@ -2381,7 +2487,7 @@ function parseVersion2(version) {
|
|
|
2381
2487
|
var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
|
|
2382
2488
|
var init_version2 = __esm(() => {
|
|
2383
2489
|
init_output();
|
|
2384
|
-
currentVersion2 = "0.
|
|
2490
|
+
currentVersion2 = "0.77.0".trim() ? "0.77.0".trim() : "0.1.0";
|
|
2385
2491
|
});
|
|
2386
2492
|
|
|
2387
2493
|
// ../../packages/registry/src/index.ts
|
|
@@ -3606,6 +3712,7 @@ var init_preview_archive = __esm(() => {
|
|
|
3606
3712
|
// src/commands/mixtape-youtube.ts
|
|
3607
3713
|
var exports_mixtape_youtube2 = {};
|
|
3608
3714
|
__export(exports_mixtape_youtube2, {
|
|
3715
|
+
resyncYoutube: () => resyncYoutube2,
|
|
3609
3716
|
publishYoutubeCommand: () => publishYoutubeCommand2,
|
|
3610
3717
|
nextOffset: () => nextOffset2,
|
|
3611
3718
|
distributeYoutube: () => distributeYoutube2,
|
|
@@ -3661,6 +3768,10 @@ async function publishYoutubeCommand2(idOrLogId) {
|
|
|
3661
3768
|
const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/publish`);
|
|
3662
3769
|
return { url: response.url };
|
|
3663
3770
|
}
|
|
3771
|
+
async function resyncYoutube2(mixtapeId) {
|
|
3772
|
+
const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/resync`);
|
|
3773
|
+
return { url: response.url, videoId: response.videoId };
|
|
3774
|
+
}
|
|
3664
3775
|
async function authYoutubeCommand2() {
|
|
3665
3776
|
const response = await adminApiGet("/api/admin/youtube/auth/start");
|
|
3666
3777
|
console.log(`Open this YouTube authorization URL:
|
|
@@ -3768,6 +3879,7 @@ var exports_mixtapes2 = {};
|
|
|
3768
3879
|
__export(exports_mixtapes2, {
|
|
3769
3880
|
mixtapesCommand: () => mixtapesCommand2,
|
|
3770
3881
|
mixtapeUpdateCommand: () => mixtapeUpdateCommand2,
|
|
3882
|
+
mixtapeResyncCommand: () => mixtapeResyncCommand2,
|
|
3771
3883
|
mixtapePublishCommand: () => mixtapePublishCommand2,
|
|
3772
3884
|
mixtapeMembersCommand: () => mixtapeMembersCommand2,
|
|
3773
3885
|
mixtapeListCommand: () => mixtapeListCommand,
|
|
@@ -3874,6 +3986,44 @@ async function mixtapeDistributeCommand2(idOrLogId, options, onProgress = () =>
|
|
|
3874
3986
|
}
|
|
3875
3987
|
return { logId, mixtapeId, results };
|
|
3876
3988
|
}
|
|
3989
|
+
async function mixtapeResyncCommand2(idOrLogId, options, onProgress = () => {}) {
|
|
3990
|
+
const mixtape = await mixtapeGetCommand(idOrLogId);
|
|
3991
|
+
if (!mixtape.id) {
|
|
3992
|
+
throw new CliError2("mixtape_not_found", `No mixtape with id or log id ${idOrLogId}`);
|
|
3993
|
+
}
|
|
3994
|
+
if (!mixtape.logId) {
|
|
3995
|
+
throw new CliError2("mixtape_no_log_id", "The mixtape isn't published yet — distribute it before re-syncing.");
|
|
3996
|
+
}
|
|
3997
|
+
const mixtapeId = mixtape.id;
|
|
3998
|
+
const explicit = Boolean(options.youtube) || Boolean(options.mixcloud);
|
|
3999
|
+
let doYoutube = Boolean(options.youtube);
|
|
4000
|
+
let doMixcloud = Boolean(options.mixcloud);
|
|
4001
|
+
if (!explicit) {
|
|
4002
|
+
const social = await adminApiGet(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/social`);
|
|
4003
|
+
const platforms = new Set(social.posts.map((post) => post.platform));
|
|
4004
|
+
doYoutube = platforms.has("youtube");
|
|
4005
|
+
doMixcloud = platforms.has("mixcloud");
|
|
4006
|
+
if (!doYoutube && !doMixcloud) {
|
|
4007
|
+
throw new CliError2("mixtape_not_distributed", "The mixtape has no YouTube or Mixcloud link to re-sync.");
|
|
4008
|
+
}
|
|
4009
|
+
}
|
|
4010
|
+
const results = [];
|
|
4011
|
+
if (doYoutube) {
|
|
4012
|
+
onProgress("YouTube: re-syncing description + chapters…");
|
|
4013
|
+
const { resyncYoutube: resyncYoutube3 } = await Promise.resolve().then(() => (init_mixtape_youtube(), exports_mixtape_youtube));
|
|
4014
|
+
const result = await resyncYoutube3(mixtapeId);
|
|
4015
|
+
results.push({ platform: "youtube", url: result.url });
|
|
4016
|
+
onProgress(`YouTube: ${result.url}`);
|
|
4017
|
+
}
|
|
4018
|
+
if (doMixcloud) {
|
|
4019
|
+
onProgress("Mixcloud: re-syncing sections…");
|
|
4020
|
+
const { resyncMixcloud: resyncMixcloud2 } = await Promise.resolve().then(() => (init_mixtape_mixcloud(), exports_mixtape_mixcloud));
|
|
4021
|
+
const result = await resyncMixcloud2(mixtapeId, onProgress);
|
|
4022
|
+
results.push({ platform: "mixcloud", url: result.url });
|
|
4023
|
+
onProgress(`Mixcloud: ${result.url}`);
|
|
4024
|
+
}
|
|
4025
|
+
return { logId: mixtape.logId, mixtapeId, results };
|
|
4026
|
+
}
|
|
3877
4027
|
async function probeDurationMs2(filePath) {
|
|
3878
4028
|
try {
|
|
3879
4029
|
const proc = Bun.spawn(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", filePath], { stderr: "ignore", stdout: "pipe" });
|
|
@@ -4035,20 +4185,42 @@ function brandDrawtext(options) {
|
|
|
4035
4185
|
const value = escapeDrawtextValue(options.text);
|
|
4036
4186
|
const font = options.fontFile ? `:fontfile='${options.fontFile}'` : "";
|
|
4037
4187
|
const border = options.borderw && options.borderw > 0 ? `:borderw=${options.borderw}:bordercolor=${CLIP_HALO_COLOR}` : "";
|
|
4038
|
-
|
|
4188
|
+
const enable = options.enable ? `:enable='${options.enable}'` : "";
|
|
4189
|
+
return `drawtext=text='${value}'${font}:` + `fontcolor=${options.color}:fontsize=${options.size}:` + `x=${options.x}:y=${options.y}${border}${enable}`;
|
|
4190
|
+
}
|
|
4191
|
+
function resolveTitleLines(options, y) {
|
|
4192
|
+
const inMs = options.inMs ?? 0;
|
|
4193
|
+
const outMs = options.outMs ?? 0;
|
|
4194
|
+
const clipDur = Math.max(0, (outMs - inMs) / 1000);
|
|
4195
|
+
const base = { fontFile: options.sansFontFile, size: CLIP_TITLE_SIZE, x: CLIP_MARGIN_X, y };
|
|
4196
|
+
const resolved = options.members && options.members.length > 0 ? resolveClipTracks({
|
|
4197
|
+
inMs,
|
|
4198
|
+
members: options.members,
|
|
4199
|
+
outMs,
|
|
4200
|
+
setDurationMs: options.setDurationMs ?? 0
|
|
4201
|
+
}) : [];
|
|
4202
|
+
if (resolved.length === 0) {
|
|
4203
|
+
return [{ ...base, text: options.title }];
|
|
4204
|
+
}
|
|
4205
|
+
const clamp = (seconds) => Math.min(Math.max(seconds, 0), clipDur);
|
|
4206
|
+
return resolved.map((track, index) => {
|
|
4207
|
+
const relStart = clamp((track.startMs - inMs) / 1000);
|
|
4208
|
+
const next = resolved[index + 1];
|
|
4209
|
+
const relEnd = next ? clamp((next.startMs - inMs) / 1000) : clipDur;
|
|
4210
|
+
return {
|
|
4211
|
+
...base,
|
|
4212
|
+
enable: `between(t,${relStart.toFixed(3)},${relEnd.toFixed(3)})`,
|
|
4213
|
+
text: track.label
|
|
4214
|
+
};
|
|
4215
|
+
});
|
|
4039
4216
|
}
|
|
4040
4217
|
function clipCutFilterComplex(options) {
|
|
4041
4218
|
const xOffset = Math.max(0, Math.round(options.xOffset));
|
|
4042
4219
|
const crop = `crop=ih*9/16:ih:${xOffset}:0,scale=${CLIP_WIDTH}:${CLIP_HEIGHT}`;
|
|
4043
4220
|
const coordinateBox = Math.round(CLIP_COORDINATE_SIZE * 1.18);
|
|
4044
4221
|
const titleBottomOffset = CLIP_SAFE_BOTTOM + coordinateBox + CLIP_LINE_GAP;
|
|
4045
|
-
const
|
|
4046
|
-
|
|
4047
|
-
size: CLIP_TITLE_SIZE,
|
|
4048
|
-
text: options.title,
|
|
4049
|
-
x: CLIP_MARGIN_X,
|
|
4050
|
-
y: `h-${titleBottomOffset}-th`
|
|
4051
|
-
};
|
|
4222
|
+
const titleY = `h-${titleBottomOffset}-th`;
|
|
4223
|
+
const titleLines = resolveTitleLines(options, titleY);
|
|
4052
4224
|
const coordinate2 = {
|
|
4053
4225
|
fontFile: options.oxaniumFontFile,
|
|
4054
4226
|
size: CLIP_COORDINATE_SIZE,
|
|
@@ -4057,11 +4229,11 @@ function clipCutFilterComplex(options) {
|
|
|
4057
4229
|
y: `h-${CLIP_SAFE_BOTTOM}-th`
|
|
4058
4230
|
};
|
|
4059
4231
|
const haloGlyphs = [
|
|
4060
|
-
brandDrawtext({ ...
|
|
4232
|
+
...titleLines.map((line) => brandDrawtext({ ...line, color: CLIP_HALO_COLOR })),
|
|
4061
4233
|
brandDrawtext({ ...coordinate2, color: CLIP_HALO_COLOR })
|
|
4062
4234
|
].join(",");
|
|
4063
4235
|
const sharpGlyphs = [
|
|
4064
|
-
brandDrawtext({ ...
|
|
4236
|
+
...titleLines.map((line) => brandDrawtext({ ...line, borderw: CLIP_SHARP_BORDERW, color: CLIP_TITLE_COLOR })),
|
|
4065
4237
|
brandDrawtext({ ...coordinate2, borderw: CLIP_SHARP_BORDERW, color: CLIP_COORDINATE_COLOR })
|
|
4066
4238
|
].join(",");
|
|
4067
4239
|
return [
|
|
@@ -4148,10 +4320,12 @@ async function clipCutCommand(clipId, onProgress = () => {}) {
|
|
|
4148
4320
|
await runClipCut({
|
|
4149
4321
|
inMs: clip.inMs,
|
|
4150
4322
|
logId: mixtape.logId,
|
|
4323
|
+
members: mixtape.members,
|
|
4151
4324
|
outMs: clip.outMs,
|
|
4152
4325
|
outputPath,
|
|
4153
4326
|
oxaniumFontFile: resolveClipFontFile(),
|
|
4154
4327
|
sansFontFile: resolveClipSansFontFile(),
|
|
4328
|
+
setDurationMs: mixtape.durationMs ?? 0,
|
|
4155
4329
|
setUrl,
|
|
4156
4330
|
title: mixtape.title,
|
|
4157
4331
|
xOffset: clip.xOffset
|
|
@@ -4214,6 +4388,7 @@ async function runClipCut(options) {
|
|
|
4214
4388
|
}
|
|
4215
4389
|
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";
|
|
4216
4390
|
var init_clips = __esm(() => {
|
|
4391
|
+
init_util();
|
|
4217
4392
|
init_api();
|
|
4218
4393
|
init_output();
|
|
4219
4394
|
MAX_CLIP_BYTES = 100 * 1024 * 1024;
|
|
@@ -4435,8 +4610,11 @@ var init_auth = __esm(() => {
|
|
|
4435
4610
|
// src/commands/mixtape-mixcloud.ts
|
|
4436
4611
|
var exports_mixtape_mixcloud2 = {};
|
|
4437
4612
|
__export(exports_mixtape_mixcloud2, {
|
|
4613
|
+
resyncMixcloud: () => resyncMixcloud2,
|
|
4438
4614
|
mixtapeDescription: () => mixtapeDescription2,
|
|
4439
4615
|
mixcloudSections: () => mixcloudSections2,
|
|
4616
|
+
mixcloudSectionFields: () => mixcloudSectionFields2,
|
|
4617
|
+
mixcloudEditUrl: () => mixcloudEditUrl2,
|
|
4440
4618
|
distributeMixcloud: () => distributeMixcloud2,
|
|
4441
4619
|
authMixcloudCommand: () => authMixcloudCommand2
|
|
4442
4620
|
});
|
|
@@ -4464,10 +4642,8 @@ async function distributeMixcloud2(mixtapeId, audioPath, onProgress, unlisted =
|
|
|
4464
4642
|
form.append(`tags-${index}-tag`, tag);
|
|
4465
4643
|
}
|
|
4466
4644
|
const sections = mixcloudSections2(mixtape.members);
|
|
4467
|
-
for (const [
|
|
4468
|
-
form.append(
|
|
4469
|
-
form.append(`sections-${index}-song`, section.song);
|
|
4470
|
-
form.append(`sections-${index}-start_time`, String(section.start_time));
|
|
4645
|
+
for (const [name, value] of mixcloudSectionFields2(sections)) {
|
|
4646
|
+
form.append(name, value);
|
|
4471
4647
|
}
|
|
4472
4648
|
if (unlisted) {
|
|
4473
4649
|
form.append("unlisted", "1");
|
|
@@ -4499,6 +4675,35 @@ async function distributeMixcloud2(mixtapeId, audioPath, onProgress, unlisted =
|
|
|
4499
4675
|
});
|
|
4500
4676
|
return { url };
|
|
4501
4677
|
}
|
|
4678
|
+
async function resyncMixcloud2(mixtapeId, onProgress) {
|
|
4679
|
+
const token = await fetchMixcloudToken2();
|
|
4680
|
+
const mixtape = await mixtapeGetCommand(mixtapeId);
|
|
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}` };
|
|
4706
|
+
}
|
|
4502
4707
|
async function authMixcloudCommand2() {
|
|
4503
4708
|
const response = await adminApiGet("/api/admin/mixcloud/auth/start");
|
|
4504
4709
|
console.log(`Open this Mixcloud authorization URL:
|
|
@@ -4529,6 +4734,18 @@ function mixcloudSections2(members) {
|
|
|
4529
4734
|
start_time: Math.floor(member.startMs / 1000)
|
|
4530
4735
|
}));
|
|
4531
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
|
+
}
|
|
4532
4749
|
function mixtapeTags2(_mixtape) {
|
|
4533
4750
|
return ["Drum & Bass", "Fluncle"];
|
|
4534
4751
|
}
|
|
@@ -7250,6 +7467,10 @@ function addAdminCommands(program2) {
|
|
|
7250
7467
|
const { publishYoutubeCommand: publishYoutubeCommand3 } = await Promise.resolve().then(() => (init_mixtape_youtube2(), exports_mixtape_youtube2));
|
|
7251
7468
|
await runMixtapePublishYoutube(idOrLogId, options, publishYoutubeCommand3);
|
|
7252
7469
|
});
|
|
7470
|
+
adminMixtapes.command("resync").description("Re-push a published mixtape's YouTube chapters + Mixcloud sections from its current cues (no re-upload)").argument("[idOrLogId]").option("--youtube", "Only re-sync YouTube").option("--mixcloud", "Only re-sync Mixcloud").option("--json", "Print JSON", false).allowExcessArguments().action(async (idOrLogId, options) => {
|
|
7471
|
+
const { mixtapeResyncCommand: mixtapeResyncCommand3 } = await Promise.resolve().then(() => (init_mixtapes(), exports_mixtapes));
|
|
7472
|
+
await runMixtapeResync(idOrLogId, options, mixtapeResyncCommand3);
|
|
7473
|
+
});
|
|
7253
7474
|
const adminClips = configureCommand(admin.command("clips").description("Mixtape clip (Fluncle Studio) commands"));
|
|
7254
7475
|
adminClips.action(() => {
|
|
7255
7476
|
adminClips.outputHelp();
|
|
@@ -7809,6 +8030,21 @@ async function runMixtapePublishYoutube(idOrLogId, options, publishYoutubeComman
|
|
|
7809
8030
|
}
|
|
7810
8031
|
console.log(`YouTube video is now public: ${result.url}`);
|
|
7811
8032
|
}
|
|
8033
|
+
async function runMixtapeResync(idOrLogId, options, mixtapeResyncCommand3) {
|
|
8034
|
+
if (!idOrLogId) {
|
|
8035
|
+
throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes resync <idOrLogId>");
|
|
8036
|
+
}
|
|
8037
|
+
const onProgress = options.json ? () => {} : (message) => console.log(message);
|
|
8038
|
+
const result = await mixtapeResyncCommand3(idOrLogId, options, onProgress);
|
|
8039
|
+
if (options.json) {
|
|
8040
|
+
printJson(result);
|
|
8041
|
+
return;
|
|
8042
|
+
}
|
|
8043
|
+
const links = result.results.map((r) => ` ${r.platform}: ${r.url}`).join(`
|
|
8044
|
+
`);
|
|
8045
|
+
console.log(`Re-synced ${result.logId} (fluncle://${result.logId}).
|
|
8046
|
+
${links}`);
|
|
8047
|
+
}
|
|
7812
8048
|
async function runMixtapeDelete(id, options, mixtapeDeleteCommand3) {
|
|
7813
8049
|
if (!id) {
|
|
7814
8050
|
throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes delete <id> --yes");
|
package/package.json
CHANGED