fluncle 0.112.0 → 0.114.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 +173 -8
- package/package.json +1 -1
package/bin/fluncle.mjs
CHANGED
|
@@ -557,7 +557,7 @@ function parseVersion(version) {
|
|
|
557
557
|
var currentVersion;
|
|
558
558
|
var init_version = __esm(() => {
|
|
559
559
|
init_output();
|
|
560
|
-
currentVersion = "0.
|
|
560
|
+
currentVersion = "0.114.0".trim() ? "0.114.0".trim() : "0.1.0";
|
|
561
561
|
});
|
|
562
562
|
|
|
563
563
|
// src/update-notifier.ts
|
|
@@ -2209,7 +2209,7 @@ function parseVersion2(version) {
|
|
|
2209
2209
|
var currentVersion2, latestReleaseUrl = "https://api.github.com/repos/mauricekleine/fluncle/releases/latest";
|
|
2210
2210
|
var init_version2 = __esm(() => {
|
|
2211
2211
|
init_output();
|
|
2212
|
-
currentVersion2 = "0.
|
|
2212
|
+
currentVersion2 = "0.114.0".trim() ? "0.114.0".trim() : "0.1.0";
|
|
2213
2213
|
});
|
|
2214
2214
|
|
|
2215
2215
|
// ../../packages/registry/src/index.ts
|
|
@@ -2244,6 +2244,18 @@ var init_src = __esm(() => {
|
|
|
2244
2244
|
url: `${SITE}/log`,
|
|
2245
2245
|
weights: { ssh: "secondary", web: "primary" }
|
|
2246
2246
|
},
|
|
2247
|
+
{
|
|
2248
|
+
exposedContent: [
|
|
2249
|
+
"Fluncle's Logbook — the voyage as a first-person travelogue, one entry per sector-day",
|
|
2250
|
+
"/logbook/:sector — one day written up, the findings inlined as photos"
|
|
2251
|
+
],
|
|
2252
|
+
kind: "web_route",
|
|
2253
|
+
name: "web.logbook",
|
|
2254
|
+
probeConfig: { cadenceMs: PROBE_CADENCE_MS, kind: "http", timeoutMs: PROBE_TIMEOUT_MS },
|
|
2255
|
+
route: "/logbook",
|
|
2256
|
+
url: `${SITE}/logbook`,
|
|
2257
|
+
weights: { ssh: "tertiary", web: "primary" }
|
|
2258
|
+
},
|
|
2247
2259
|
{
|
|
2248
2260
|
exposedContent: ["Fluncle's own DJ mixtapes — each a checkpoint set with an F-marked Log ID"],
|
|
2249
2261
|
kind: "web_route",
|
|
@@ -2253,6 +2265,18 @@ var init_src = __esm(() => {
|
|
|
2253
2265
|
url: `${SITE}/mixtapes`,
|
|
2254
2266
|
weights: { ssh: "secondary", web: "primary" }
|
|
2255
2267
|
},
|
|
2268
|
+
{
|
|
2269
|
+
exposedContent: [
|
|
2270
|
+
"the mixability set-builder — chain a set from Fluncle's findings that mixes clean, share it as a link"
|
|
2271
|
+
],
|
|
2272
|
+
kind: "web_route",
|
|
2273
|
+
name: "web.mix",
|
|
2274
|
+
operatorNotes: "ADMIN-GATED at launch (RFC mixability-engine, Decision 1): the full /mix set-builder ships behind admin auth until the archive clears the ~250-finding floor. Registered here as `pending` (dark to every public consumer — the dev-row, llms.txt, the sitemap, /status) so going public is one flip: remove `pending` + set a web weight + announce. The `list_mixable_tracks` API op already ships at its final public tier, so the flip needs no rebuild. No probeConfig — the admin gate 302s a bare GET to /admin/login, which would read as a false 'down'.",
|
|
2275
|
+
pending: true,
|
|
2276
|
+
route: "/mix",
|
|
2277
|
+
url: `${SITE}/mix`,
|
|
2278
|
+
weights: { web: "secondary" }
|
|
2279
|
+
},
|
|
2256
2280
|
{
|
|
2257
2281
|
exposedContent: [
|
|
2258
2282
|
"the feed-first Stories surface — full-bleed findings",
|
|
@@ -2851,6 +2875,22 @@ var init_src = __esm(() => {
|
|
|
2851
2875
|
probeConfig: { cadenceMs: 10 * MINUTE_MS, cronName: "fluncle-note", kind: "cron" },
|
|
2852
2876
|
weights: { status: "hidden" }
|
|
2853
2877
|
},
|
|
2878
|
+
{
|
|
2879
|
+
command: "fluncle admin logbook gaps",
|
|
2880
|
+
exposedContent: [
|
|
2881
|
+
"author the previous day's Logbook travelogue entry, fill-empty-only (hybrid: one claude -p call)"
|
|
2882
|
+
],
|
|
2883
|
+
kind: "cron",
|
|
2884
|
+
name: "cron.logbook",
|
|
2885
|
+
operatorNotes: "00:40 Amsterdam daily. Hybrid --no-agent; one claude -p writes the day up. Never clobbers an operator entry; the self-healing gap window backfills history oldest-first. Source: docs/agents/hermes/scripts/logbook-sweep.{sh,ts}.",
|
|
2886
|
+
probeConfig: {
|
|
2887
|
+
cadenceMs: 24 * 60 * MINUTE_MS,
|
|
2888
|
+
cronName: "fluncle-logbook",
|
|
2889
|
+
kind: "cron",
|
|
2890
|
+
schedule: { time: "00:40", tz: "Europe/Amsterdam" }
|
|
2891
|
+
},
|
|
2892
|
+
weights: { status: "hidden" }
|
|
2893
|
+
},
|
|
2854
2894
|
{
|
|
2855
2895
|
command: "fluncle admin tracks observe --queue",
|
|
2856
2896
|
exposedContent: [
|
|
@@ -3458,6 +3498,7 @@ __export(exports_admin_tracks, {
|
|
|
3458
3498
|
queueCommand: () => queueCommand,
|
|
3459
3499
|
observeQueueCommand: () => observeQueueCommand,
|
|
3460
3500
|
noteQueueCommand: () => noteQueueCommand,
|
|
3501
|
+
mixableOrderCommand: () => mixableOrderCommand,
|
|
3461
3502
|
listCommand: () => listCommand,
|
|
3462
3503
|
enrichQueueCommand: () => enrichQueueCommand,
|
|
3463
3504
|
embedQueueCommand: () => embedQueueCommand,
|
|
@@ -3637,6 +3678,13 @@ async function vehiclesCommand(limit) {
|
|
|
3637
3678
|
vehicle: track.videoVehicle
|
|
3638
3679
|
}));
|
|
3639
3680
|
}
|
|
3681
|
+
async function mixableOrderCommand(logIds, seed) {
|
|
3682
|
+
const params = new URLSearchParams({ ids: logIds.join(",") });
|
|
3683
|
+
if (seed) {
|
|
3684
|
+
params.set("seed", seed);
|
|
3685
|
+
}
|
|
3686
|
+
return adminApiGet(`/api/admin/tracks/mixable-order?${params.toString()}`);
|
|
3687
|
+
}
|
|
3640
3688
|
var pageSize3 = 48;
|
|
3641
3689
|
var init_admin_tracks = __esm(() => {
|
|
3642
3690
|
init_api();
|
|
@@ -4821,6 +4869,54 @@ var init_newsletter = __esm(() => {
|
|
|
4821
4869
|
init_output();
|
|
4822
4870
|
});
|
|
4823
4871
|
|
|
4872
|
+
// src/commands/admin-logbook.ts
|
|
4873
|
+
var exports_admin_logbook = {};
|
|
4874
|
+
__export(exports_admin_logbook, {
|
|
4875
|
+
logbookUpdateCommand: () => logbookUpdateCommand,
|
|
4876
|
+
logbookGapsCommand: () => logbookGapsCommand,
|
|
4877
|
+
logbookCreateCommand: () => logbookCreateCommand
|
|
4878
|
+
});
|
|
4879
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
4880
|
+
async function logbookGapsCommand(limit) {
|
|
4881
|
+
const query = typeof limit === "number" ? `?limit=${encodeURIComponent(String(limit))}` : "";
|
|
4882
|
+
const response = await adminApiGet(`/api/admin/logbook/gaps${query}`);
|
|
4883
|
+
return response.gaps;
|
|
4884
|
+
}
|
|
4885
|
+
function resolveBody(options) {
|
|
4886
|
+
if (options.body !== undefined) {
|
|
4887
|
+
return options.body;
|
|
4888
|
+
}
|
|
4889
|
+
if (options.bodyFile !== undefined) {
|
|
4890
|
+
if (!existsSync5(options.bodyFile)) {
|
|
4891
|
+
throw new CliError2("file_not_found", `Body file not found: ${options.bodyFile}`);
|
|
4892
|
+
}
|
|
4893
|
+
return readFileSync5(options.bodyFile, "utf-8");
|
|
4894
|
+
}
|
|
4895
|
+
throw new CliError2("missing_body", "An entry needs a body via --body <text> or --body-file <entry.md>");
|
|
4896
|
+
}
|
|
4897
|
+
function requireTitle(options) {
|
|
4898
|
+
if (options.title === undefined || !options.title.trim()) {
|
|
4899
|
+
throw new CliError2("missing_title", "An entry needs a title via --title <text>");
|
|
4900
|
+
}
|
|
4901
|
+
return options.title;
|
|
4902
|
+
}
|
|
4903
|
+
async function logbookCreateCommand(sector, options) {
|
|
4904
|
+
return adminApiPost(`/api/admin/logbook/${encodeURIComponent(sector)}`, {
|
|
4905
|
+
body: resolveBody(options),
|
|
4906
|
+
title: requireTitle(options)
|
|
4907
|
+
});
|
|
4908
|
+
}
|
|
4909
|
+
async function logbookUpdateCommand(sector, options) {
|
|
4910
|
+
return adminApiPatch(`/api/admin/logbook/${encodeURIComponent(sector)}`, {
|
|
4911
|
+
body: resolveBody(options),
|
|
4912
|
+
title: requireTitle(options)
|
|
4913
|
+
});
|
|
4914
|
+
}
|
|
4915
|
+
var init_admin_logbook = __esm(() => {
|
|
4916
|
+
init_api();
|
|
4917
|
+
init_output();
|
|
4918
|
+
});
|
|
4919
|
+
|
|
4824
4920
|
// src/commands/add.ts
|
|
4825
4921
|
async function addCommand2(spotifyUrl, options) {
|
|
4826
4922
|
const result = await adminApiPost("/api/admin/tracks", {
|
|
@@ -5485,7 +5581,7 @@ var init_format2 = __esm(() => {
|
|
|
5485
5581
|
});
|
|
5486
5582
|
|
|
5487
5583
|
// src/cli.ts
|
|
5488
|
-
import { existsSync as
|
|
5584
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
5489
5585
|
import path2 from "path";
|
|
5490
5586
|
|
|
5491
5587
|
// ../../node_modules/commander/lib/error.js
|
|
@@ -7801,6 +7897,10 @@ function addAdminCommands(program2) {
|
|
|
7801
7897
|
const { trackGetAdminCommand: trackGetAdminCommand2 } = await Promise.resolve().then(() => (init_track(), exports_track));
|
|
7802
7898
|
await runTrackGetAdmin(idOrLogId, options, trackGetAdminCommand2);
|
|
7803
7899
|
});
|
|
7900
|
+
adminTrack.command("mixable-order").description("Order findings into a smooth proposed mix (2\u201364 Log IDs)").argument("[logIds...]", "The findings to order, by Log ID").option("--seed <logId>", "Pin the first stop").option("--json", "Print JSON", false).allowExcessArguments().action(async (logIds, options) => {
|
|
7901
|
+
const { mixableOrderCommand: mixableOrderCommand2 } = await Promise.resolve().then(() => (init_admin_tracks(), exports_admin_tracks));
|
|
7902
|
+
await runMixableOrder(logIds, options, mixableOrderCommand2);
|
|
7903
|
+
});
|
|
7804
7904
|
adminTrack.command("update").description("Certify a track into the archive").argument("[trackId]").option("--analyzed-at <iso>", "Analysis-write ISO timestamp (BPM/key provenance)").option("--analyzed-from <class>", "Audio class BPM/key were analyzed from: full or preview").option("--bpm <number>", "Track BPM").option("--bpm-confidence <number>", "Analyzer confidence in the BPM (0..1)").option("--bpm-source <source>", "Where the BPM came from (analyzer bpmSource)").option("--embedding <json>", "MuQ audio embedding as a JSON array of 1024 floats").option("--embedding-file <file>", "Read the MuQ embedding JSON array from a file").option("--features <json>", "Audio feature JSON").option("--json", "Print JSON", false).option("--key <key>", "Musical key").option("--key-confidence <number>", "Analyzer confidence in the key (0..1)").option("--key-source <source>", "Where the key came from (analyzer keySource)").option("--note <text>", "Operator note").option("--status <status>", "Enrichment status").option("--video-url <url>", "Rendered video URL").allowExcessArguments().action(async (trackId, options) => {
|
|
7805
7905
|
const { trackUpdateCommand: trackUpdateCommand3 } = await Promise.resolve().then(() => (init_track(), exports_track));
|
|
7806
7906
|
await runTrackUpdate(trackId, options, trackUpdateCommand3);
|
|
@@ -7969,6 +8069,22 @@ function addAdminCommands(program2) {
|
|
|
7969
8069
|
const { newsletterDeleteCommand: newsletterDeleteCommand2 } = await Promise.resolve().then(() => (init_newsletter(), exports_newsletter));
|
|
7970
8070
|
await runNewsletterDelete(id, options, newsletterDeleteCommand2);
|
|
7971
8071
|
});
|
|
8072
|
+
const adminLogbook = configureCommand(admin.command("logbook").description("Fluncle's Logbook (travelogue) commands"));
|
|
8073
|
+
adminLogbook.action(() => {
|
|
8074
|
+
adminLogbook.outputHelp();
|
|
8075
|
+
});
|
|
8076
|
+
adminLogbook.command("gaps").description("List sector-days with findings but no logbook entry (the sweep's worklist)").option("--limit <limit>", "Max gaps to return").option("--json", "Print JSON", false).allowExcessArguments().action(async (options) => {
|
|
8077
|
+
const { logbookGapsCommand: logbookGapsCommand2 } = await Promise.resolve().then(() => (init_admin_logbook(), exports_admin_logbook));
|
|
8078
|
+
await runLogbookGaps(options, logbookGapsCommand2);
|
|
8079
|
+
});
|
|
8080
|
+
adminLogbook.command("create").description("Author a sector-day's entry (fills an empty sector only)").argument("[sector]").option("--title <text>", "The entry title").option("--body <text>", "The entry body (markdown; [[logId]] figure tokens)").option("--body-file <file>", "Read the body from a file (the sweep's path)").option("--json", "Print JSON", false).allowExcessArguments().action(async (sector, options) => {
|
|
8081
|
+
const { logbookCreateCommand: logbookCreateCommand2 } = await Promise.resolve().then(() => (init_admin_logbook(), exports_admin_logbook));
|
|
8082
|
+
await runLogbookWrite(sector, options, logbookCreateCommand2);
|
|
8083
|
+
});
|
|
8084
|
+
adminLogbook.command("update").description("Create or overwrite a sector-day's entry. OPERATOR only").argument("[sector]").option("--title <text>", "The entry title").option("--body <text>", "The entry body (markdown; [[logId]] figure tokens)").option("--body-file <file>", "Read the body from a file").option("--json", "Print JSON", false).allowExcessArguments().action(async (sector, options) => {
|
|
8085
|
+
const { logbookUpdateCommand: logbookUpdateCommand2 } = await Promise.resolve().then(() => (init_admin_logbook(), exports_admin_logbook));
|
|
8086
|
+
await runLogbookWrite(sector, options, logbookUpdateCommand2);
|
|
8087
|
+
});
|
|
7972
8088
|
const submissions = configureCommand(admin.command("submissions").description("Review listener submissions"));
|
|
7973
8089
|
submissions.option("--json", "Print JSON", false).action(async (options) => {
|
|
7974
8090
|
const { listSubmissionsCommand: listSubmissionsCommand2 } = await Promise.resolve().then(() => (init_submissions(), exports_submissions));
|
|
@@ -7999,7 +8115,7 @@ function addAdminCommands(program2) {
|
|
|
7999
8115
|
if (!submissionId) {
|
|
8000
8116
|
throw new Error("Missing submission id for: triage");
|
|
8001
8117
|
}
|
|
8002
|
-
const verdict = options.verdictFile ?
|
|
8118
|
+
const verdict = options.verdictFile ? readFileSync6(options.verdictFile, "utf8") : options.verdict;
|
|
8003
8119
|
if (!verdict || !verdict.trim()) {
|
|
8004
8120
|
throw new Error("Usage: fluncle admin submissions triage <submissionId> (--verdict <text> | --verdict-file <file>) [--json]");
|
|
8005
8121
|
}
|
|
@@ -8084,7 +8200,7 @@ async function runTrackPreviewArchive(idOrLogId, options, previewArchiveUploadCo
|
|
|
8084
8200
|
console.log(` mime: ${result.mime}`);
|
|
8085
8201
|
}
|
|
8086
8202
|
async function runTrackObserve(idOrLogId, options, trackObserveCommand2) {
|
|
8087
|
-
const script = options.scriptFile ?
|
|
8203
|
+
const script = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
|
|
8088
8204
|
if (!idOrLogId || !script || !script.trim()) {
|
|
8089
8205
|
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]");
|
|
8090
8206
|
}
|
|
@@ -8140,7 +8256,7 @@ async function runTrackContext(idOrLogId, options, trackContextCommand2) {
|
|
|
8140
8256
|
}
|
|
8141
8257
|
}
|
|
8142
8258
|
async function runTrackNote(idOrLogId, options, trackNoteCommand2) {
|
|
8143
|
-
const note = options.scriptFile ?
|
|
8259
|
+
const note = options.scriptFile ? readFileSync6(options.scriptFile, "utf8") : options.script;
|
|
8144
8260
|
if (!idOrLogId || !note || !note.trim()) {
|
|
8145
8261
|
throw new Error("Usage: fluncle admin tracks note <track_id|log_id> (--script <text> | --script-file <file>) [--json]");
|
|
8146
8262
|
}
|
|
@@ -8453,7 +8569,7 @@ async function runTrackVideo(idOrLogId, options, trackVideoCommand2) {
|
|
|
8453
8569
|
return;
|
|
8454
8570
|
}
|
|
8455
8571
|
const candidate = path2.join(dir, name);
|
|
8456
|
-
return
|
|
8572
|
+
return existsSync6(candidate) ? candidate : undefined;
|
|
8457
8573
|
};
|
|
8458
8574
|
const resolveFile = (explicit, name) => {
|
|
8459
8575
|
if (explicit) {
|
|
@@ -8592,6 +8708,24 @@ async function runTrackGet(idOrLogId, options, trackGetCommand2) {
|
|
|
8592
8708
|
console.log(`Log: ${t.logPageUrl}`);
|
|
8593
8709
|
}
|
|
8594
8710
|
}
|
|
8711
|
+
async function runMixableOrder(logIds, options, mixableOrderCommand2) {
|
|
8712
|
+
if (logIds.length < 2) {
|
|
8713
|
+
throw new Error("Give 2 or more Log IDs. Usage: fluncle admin tracks mixable-order <logId...> [--seed <logId>] [--json]");
|
|
8714
|
+
}
|
|
8715
|
+
const result = await mixableOrderCommand2(logIds, options.seed);
|
|
8716
|
+
if (options.json) {
|
|
8717
|
+
printJson(result);
|
|
8718
|
+
return;
|
|
8719
|
+
}
|
|
8720
|
+
console.log(`Proposed mix \u2014 a smooth chain (${result.algorithm}), not an energy-shaped set. Copy into Rekordbox as advisory.
|
|
8721
|
+
`);
|
|
8722
|
+
result.order.forEach((stop, index) => {
|
|
8723
|
+
const into = index === 0 ? "start" : stop.flagged ? "sparse join" : stop.transitionReason?.relationship.replace(/_/g, " ") ?? "\u2014";
|
|
8724
|
+
const meta = [stop.key, stop.bpm ? `${Math.round(stop.bpm)} bpm` : undefined].filter(Boolean).join(" \xB7 ");
|
|
8725
|
+
console.log(`${String(index + 1).padStart(2)}. ${stop.logId} ${stop.artists.join(", ")} \u2014 ${stop.title}`);
|
|
8726
|
+
console.log(` ${[into, meta].filter(Boolean).join(" \xB7 ")}`);
|
|
8727
|
+
});
|
|
8728
|
+
}
|
|
8595
8729
|
async function runTrackGetAdmin(idOrLogId, options, trackGetAdminCommand2) {
|
|
8596
8730
|
if (!idOrLogId) {
|
|
8597
8731
|
throw new Error("Missing id. Usage: fluncle admin tracks get <track_id|log_id> [--json]");
|
|
@@ -8648,7 +8782,7 @@ async function runTrackUpdate(trackId, options, trackUpdateCommand3) {
|
|
|
8648
8782
|
if (options.analyzedFrom !== undefined && options.analyzedFrom !== "full" && options.analyzedFrom !== "preview") {
|
|
8649
8783
|
throw new Error(`Invalid --analyzed-from: ${options.analyzedFrom} (expected full or preview)`);
|
|
8650
8784
|
}
|
|
8651
|
-
const embeddingRaw = options.embeddingFile ?
|
|
8785
|
+
const embeddingRaw = options.embeddingFile ? readFileSync6(options.embeddingFile, "utf8") : options.embedding;
|
|
8652
8786
|
const embedding = parseEmbeddingArg(embeddingRaw);
|
|
8653
8787
|
const result = await trackUpdateCommand3(trackId, {
|
|
8654
8788
|
analyzedAt: options.analyzedAt,
|
|
@@ -8911,6 +9045,37 @@ async function runNewsletterList(options, newsletterListCommand2) {
|
|
|
8911
9045
|
console.log(`${label} ${edition.status} ${edition.id} ${subject}`);
|
|
8912
9046
|
}
|
|
8913
9047
|
}
|
|
9048
|
+
async function runLogbookGaps(options, logbookGapsCommand2) {
|
|
9049
|
+
const limit = options.limit === undefined ? undefined : Number.parseInt(options.limit, 10);
|
|
9050
|
+
const gaps = await logbookGapsCommand2(limit !== undefined && Number.isFinite(limit) ? limit : undefined);
|
|
9051
|
+
if (options.json) {
|
|
9052
|
+
printJson({ gaps, ok: true });
|
|
9053
|
+
return;
|
|
9054
|
+
}
|
|
9055
|
+
if (gaps.length === 0) {
|
|
9056
|
+
console.log("No gaps \u2014 every past sector-day with findings has an entry.");
|
|
9057
|
+
return;
|
|
9058
|
+
}
|
|
9059
|
+
for (const gap of gaps) {
|
|
9060
|
+
console.log(`sector ${gap.sector} ${gap.date.slice(0, 10)} ${gap.findings.length} findings`);
|
|
9061
|
+
}
|
|
9062
|
+
}
|
|
9063
|
+
async function runLogbookWrite(sector, options, writeCommand) {
|
|
9064
|
+
if (!sector) {
|
|
9065
|
+
throw new Error("Missing sector for the logbook entry");
|
|
9066
|
+
}
|
|
9067
|
+
const result = await writeCommand(sector, {
|
|
9068
|
+
body: options.body,
|
|
9069
|
+
bodyFile: options.bodyFile,
|
|
9070
|
+
title: options.title
|
|
9071
|
+
});
|
|
9072
|
+
if (options.json) {
|
|
9073
|
+
printJson(result);
|
|
9074
|
+
return;
|
|
9075
|
+
}
|
|
9076
|
+
const skipped = "skipped" in result && result.skipped;
|
|
9077
|
+
console.log(skipped ? `sector ${result.entry.sector}: an entry already stands \u2014 no-op (${result.entry.generatedBy})` : `sector ${result.entry.sector}: ${result.entry.title}`);
|
|
9078
|
+
}
|
|
8914
9079
|
async function runAdd(spotifyUrl, options, addCommand3) {
|
|
8915
9080
|
if (!spotifyUrl) {
|
|
8916
9081
|
throw new Error("Missing Spotify track URL");
|
package/package.json
CHANGED