fluncle 0.76.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.
Files changed (2) hide show
  1. package/bin/fluncle.mjs +201 -10
  2. package/package.json +1 -1
package/bin/fluncle.mjs CHANGED
@@ -544,7 +544,7 @@ function parseVersion(version) {
544
544
  var currentVersion;
545
545
  var init_version = __esm(() => {
546
546
  init_output();
547
- currentVersion = "0.76.0".trim() ? "0.76.0".trim() : "0.1.0";
547
+ currentVersion = "0.77.0".trim() ? "0.77.0".trim() : "0.1.0";
548
548
  });
549
549
 
550
550
  // src/update-notifier.ts
@@ -1029,6 +1029,7 @@ var init_mixtape_api = __esm(() => {
1029
1029
  // src/commands/mixtape-youtube.ts
1030
1030
  var exports_mixtape_youtube = {};
1031
1031
  __export(exports_mixtape_youtube, {
1032
+ resyncYoutube: () => resyncYoutube,
1032
1033
  publishYoutubeCommand: () => publishYoutubeCommand,
1033
1034
  nextOffset: () => nextOffset,
1034
1035
  distributeYoutube: () => distributeYoutube,
@@ -1084,6 +1085,10 @@ async function publishYoutubeCommand(idOrLogId) {
1084
1085
  const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/publish`);
1085
1086
  return { url: response.url };
1086
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
+ }
1087
1092
  async function authYoutubeCommand() {
1088
1093
  const response = await adminApiGet("/api/admin/youtube/auth/start");
1089
1094
  console.log(`Open this YouTube authorization URL:
@@ -1189,8 +1194,11 @@ var init_mixtape_youtube = __esm(() => {
1189
1194
  // src/commands/mixtape-mixcloud.ts
1190
1195
  var exports_mixtape_mixcloud = {};
1191
1196
  __export(exports_mixtape_mixcloud, {
1197
+ resyncMixcloud: () => resyncMixcloud,
1192
1198
  mixtapeDescription: () => mixtapeDescription,
1193
1199
  mixcloudSections: () => mixcloudSections,
1200
+ mixcloudSectionFields: () => mixcloudSectionFields,
1201
+ mixcloudEditUrl: () => mixcloudEditUrl,
1194
1202
  distributeMixcloud: () => distributeMixcloud,
1195
1203
  authMixcloudCommand: () => authMixcloudCommand
1196
1204
  });
@@ -1218,10 +1226,8 @@ async function distributeMixcloud(mixtapeId, audioPath, onProgress, unlisted = f
1218
1226
  form.append(`tags-${index}-tag`, tag);
1219
1227
  }
1220
1228
  const sections = mixcloudSections(mixtape.members);
1221
- for (const [index, section] of sections.entries()) {
1222
- form.append(`sections-${index}-artist`, section.artist);
1223
- form.append(`sections-${index}-song`, section.song);
1224
- form.append(`sections-${index}-start_time`, String(section.start_time));
1229
+ for (const [name, value] of mixcloudSectionFields(sections)) {
1230
+ form.append(name, value);
1225
1231
  }
1226
1232
  if (unlisted) {
1227
1233
  form.append("unlisted", "1");
@@ -1253,6 +1259,35 @@ async function distributeMixcloud(mixtapeId, audioPath, onProgress, unlisted = f
1253
1259
  });
1254
1260
  return { url };
1255
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
+ }
1256
1291
  async function authMixcloudCommand() {
1257
1292
  const response = await adminApiGet("/api/admin/mixcloud/auth/start");
1258
1293
  console.log(`Open this Mixcloud authorization URL:
@@ -1283,6 +1318,18 @@ function mixcloudSections(members) {
1283
1318
  start_time: Math.floor(member.startMs / 1000)
1284
1319
  }));
1285
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
+ }
1286
1333
  function mixtapeTags(_mixtape) {
1287
1334
  return ["Drum & Bass", "Fluncle"];
1288
1335
  }
@@ -1509,6 +1556,7 @@ var exports_mixtapes = {};
1509
1556
  __export(exports_mixtapes, {
1510
1557
  mixtapesCommand: () => mixtapesCommand,
1511
1558
  mixtapeUpdateCommand: () => mixtapeUpdateCommand,
1559
+ mixtapeResyncCommand: () => mixtapeResyncCommand,
1512
1560
  mixtapePublishCommand: () => mixtapePublishCommand,
1513
1561
  mixtapeMembersCommand: () => mixtapeMembersCommand,
1514
1562
  mixtapeListCommand: () => mixtapeListCommand,
@@ -1615,6 +1663,44 @@ async function mixtapeDistributeCommand(idOrLogId, options, onProgress = () => {
1615
1663
  }
1616
1664
  return { logId, mixtapeId, results };
1617
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
+ }
1618
1704
  async function probeDurationMs(filePath) {
1619
1705
  try {
1620
1706
  const proc = Bun.spawn(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", filePath], { stderr: "ignore", stdout: "pipe" });
@@ -2401,7 +2487,7 @@ function parseVersion2(version) {
2401
2487
  var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
2402
2488
  var init_version2 = __esm(() => {
2403
2489
  init_output();
2404
- currentVersion2 = "0.76.0".trim() ? "0.76.0".trim() : "0.1.0";
2490
+ currentVersion2 = "0.77.0".trim() ? "0.77.0".trim() : "0.1.0";
2405
2491
  });
2406
2492
 
2407
2493
  // ../../packages/registry/src/index.ts
@@ -3626,6 +3712,7 @@ var init_preview_archive = __esm(() => {
3626
3712
  // src/commands/mixtape-youtube.ts
3627
3713
  var exports_mixtape_youtube2 = {};
3628
3714
  __export(exports_mixtape_youtube2, {
3715
+ resyncYoutube: () => resyncYoutube2,
3629
3716
  publishYoutubeCommand: () => publishYoutubeCommand2,
3630
3717
  nextOffset: () => nextOffset2,
3631
3718
  distributeYoutube: () => distributeYoutube2,
@@ -3681,6 +3768,10 @@ async function publishYoutubeCommand2(idOrLogId) {
3681
3768
  const response = await adminApiPost(`/api/admin/mixtapes/${encodeURIComponent(mixtapeId)}/youtube/publish`);
3682
3769
  return { url: response.url };
3683
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
+ }
3684
3775
  async function authYoutubeCommand2() {
3685
3776
  const response = await adminApiGet("/api/admin/youtube/auth/start");
3686
3777
  console.log(`Open this YouTube authorization URL:
@@ -3788,6 +3879,7 @@ var exports_mixtapes2 = {};
3788
3879
  __export(exports_mixtapes2, {
3789
3880
  mixtapesCommand: () => mixtapesCommand2,
3790
3881
  mixtapeUpdateCommand: () => mixtapeUpdateCommand2,
3882
+ mixtapeResyncCommand: () => mixtapeResyncCommand2,
3791
3883
  mixtapePublishCommand: () => mixtapePublishCommand2,
3792
3884
  mixtapeMembersCommand: () => mixtapeMembersCommand2,
3793
3885
  mixtapeListCommand: () => mixtapeListCommand,
@@ -3894,6 +3986,44 @@ async function mixtapeDistributeCommand2(idOrLogId, options, onProgress = () =>
3894
3986
  }
3895
3987
  return { logId, mixtapeId, results };
3896
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
+ }
3897
4027
  async function probeDurationMs2(filePath) {
3898
4028
  try {
3899
4029
  const proc = Bun.spawn(["ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "csv=p=0", filePath], { stderr: "ignore", stdout: "pipe" });
@@ -4480,8 +4610,11 @@ var init_auth = __esm(() => {
4480
4610
  // src/commands/mixtape-mixcloud.ts
4481
4611
  var exports_mixtape_mixcloud2 = {};
4482
4612
  __export(exports_mixtape_mixcloud2, {
4613
+ resyncMixcloud: () => resyncMixcloud2,
4483
4614
  mixtapeDescription: () => mixtapeDescription2,
4484
4615
  mixcloudSections: () => mixcloudSections2,
4616
+ mixcloudSectionFields: () => mixcloudSectionFields2,
4617
+ mixcloudEditUrl: () => mixcloudEditUrl2,
4485
4618
  distributeMixcloud: () => distributeMixcloud2,
4486
4619
  authMixcloudCommand: () => authMixcloudCommand2
4487
4620
  });
@@ -4509,10 +4642,8 @@ async function distributeMixcloud2(mixtapeId, audioPath, onProgress, unlisted =
4509
4642
  form.append(`tags-${index}-tag`, tag);
4510
4643
  }
4511
4644
  const sections = mixcloudSections2(mixtape.members);
4512
- for (const [index, section] of sections.entries()) {
4513
- form.append(`sections-${index}-artist`, section.artist);
4514
- form.append(`sections-${index}-song`, section.song);
4515
- form.append(`sections-${index}-start_time`, String(section.start_time));
4645
+ for (const [name, value] of mixcloudSectionFields2(sections)) {
4646
+ form.append(name, value);
4516
4647
  }
4517
4648
  if (unlisted) {
4518
4649
  form.append("unlisted", "1");
@@ -4544,6 +4675,35 @@ async function distributeMixcloud2(mixtapeId, audioPath, onProgress, unlisted =
4544
4675
  });
4545
4676
  return { url };
4546
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
+ }
4547
4707
  async function authMixcloudCommand2() {
4548
4708
  const response = await adminApiGet("/api/admin/mixcloud/auth/start");
4549
4709
  console.log(`Open this Mixcloud authorization URL:
@@ -4574,6 +4734,18 @@ function mixcloudSections2(members) {
4574
4734
  start_time: Math.floor(member.startMs / 1000)
4575
4735
  }));
4576
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
+ }
4577
4749
  function mixtapeTags2(_mixtape) {
4578
4750
  return ["Drum & Bass", "Fluncle"];
4579
4751
  }
@@ -7295,6 +7467,10 @@ function addAdminCommands(program2) {
7295
7467
  const { publishYoutubeCommand: publishYoutubeCommand3 } = await Promise.resolve().then(() => (init_mixtape_youtube2(), exports_mixtape_youtube2));
7296
7468
  await runMixtapePublishYoutube(idOrLogId, options, publishYoutubeCommand3);
7297
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
+ });
7298
7474
  const adminClips = configureCommand(admin.command("clips").description("Mixtape clip (Fluncle Studio) commands"));
7299
7475
  adminClips.action(() => {
7300
7476
  adminClips.outputHelp();
@@ -7854,6 +8030,21 @@ async function runMixtapePublishYoutube(idOrLogId, options, publishYoutubeComman
7854
8030
  }
7855
8031
  console.log(`YouTube video is now public: ${result.url}`);
7856
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
+ }
7857
8048
  async function runMixtapeDelete(id, options, mixtapeDeleteCommand3) {
7858
8049
  if (!id) {
7859
8050
  throw new Error("Missing mixtape id. Usage: fluncle admin mixtapes delete <id> --yes");
package/package.json CHANGED
@@ -31,5 +31,5 @@
31
31
  "url": "git+https://github.com/mauricekleine/fluncle.git"
32
32
  },
33
33
  "type": "module",
34
- "version": "0.76.0"
34
+ "version": "0.77.0"
35
35
  }