nixamp 0.48.0 → 0.49.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 (56) hide show
  1. package/dist/ai-keys.d.ts +1 -1
  2. package/dist/ai-keys.js +2 -0
  3. package/dist/ai-label.d.ts +210 -0
  4. package/dist/ai-label.js +725 -0
  5. package/dist/ai-radio.d.ts +11 -5
  6. package/dist/ai-radio.js +27 -11
  7. package/dist/ai-text.d.ts +2 -0
  8. package/dist/ai-text.js +4 -2
  9. package/dist/label-routes.d.ts +15 -0
  10. package/dist/label-routes.js +255 -0
  11. package/dist/server.d.ts +3 -0
  12. package/dist/server.js +28 -1
  13. package/dist/station-cli.js +109 -0
  14. package/dist/station-guide.d.ts +1 -1
  15. package/dist/station-guide.js +8 -3
  16. package/dist/station-mcp.d.ts +247 -20
  17. package/dist/station-mcp.js +36 -0
  18. package/dist/station-metering.js +2 -2
  19. package/dist/station-usage.d.ts +1 -1
  20. package/dist/station-usage.js +15 -1
  21. package/package.json +1 -1
  22. package/src/ai-keys.ts +2 -0
  23. package/src/ai-label.ts +826 -0
  24. package/src/ai-radio.ts +30 -13
  25. package/src/ai-text.ts +6 -2
  26. package/src/label-routes.ts +268 -0
  27. package/src/server.ts +30 -1
  28. package/src/station-cli.ts +102 -0
  29. package/src/station-guide.ts +8 -3
  30. package/src/station-mcp.ts +34 -0
  31. package/src/station-metering.ts +2 -2
  32. package/src/station-usage.ts +15 -1
  33. package/web/dist/assets/{ar-ropwGSSV.js → ar-BEVjqkqM.js} +1 -1
  34. package/web/dist/assets/{cs-Bi7uRYjd.js → cs-DjazXQ15.js} +1 -1
  35. package/web/dist/assets/{da-B8OELPch.js → da-DnyF6SYK.js} +1 -1
  36. package/web/dist/assets/{de-DJOTeXET.js → de--q1ph9Ez.js} +1 -1
  37. package/web/dist/assets/{es-BIupzuzS.js → es-Do3DkBwD.js} +1 -1
  38. package/web/dist/assets/{fi-A6MoWkzZ.js → fi-B7sJTx1N.js} +1 -1
  39. package/web/dist/assets/{fr-DerIDIwl.js → fr-Dy7f5rGi.js} +1 -1
  40. package/web/dist/assets/{hi-CyUtRBH4.js → hi-C9vSnTQ4.js} +1 -1
  41. package/web/dist/assets/{hu-DD1vrIhY.js → hu-h-eueQE8.js} +1 -1
  42. package/web/dist/assets/{id-C6OAMzii.js → id-D8Ffd-Pc.js} +1 -1
  43. package/web/dist/assets/{it-Xy86vmU7.js → it-BKM_2rj1.js} +1 -1
  44. package/web/dist/assets/main-Dl_uRj22.js +22 -0
  45. package/web/dist/assets/{nl-4Q-K2Jf1.js → nl-Cj3ETrcl.js} +1 -1
  46. package/web/dist/assets/radio-zPgyISLf.js +1 -0
  47. package/web/dist/assets/{ru-CpxwOhC4.js → ru-B5zmGSnG.js} +1 -1
  48. package/web/dist/assets/{sv-aRvu7MWa.js → sv-BMtjA1uI.js} +1 -1
  49. package/web/dist/assets/{uk-BCwwfngg.js → uk-B2vuB6Un.js} +1 -1
  50. package/web/dist/assets/{vi-tvWPiAH_.js → vi-CXam3lDh.js} +1 -1
  51. package/web/dist/assets/{zh-Bp2aLEyX.js → zh-DnYhyzqb.js} +1 -1
  52. package/web/dist/index.html +1 -1
  53. package/web/dist/radio.html +33 -1
  54. package/web/dist/sw.js +20 -20
  55. package/web/dist/assets/main-CuG8-ADK.js +0 -22
  56. package/web/dist/assets/radio-DI0H8flN.js +0 -1
@@ -282,6 +282,8 @@ export async function station(argv) {
282
282
  }
283
283
  case "ai":
284
284
  return await aiCommand(argv.slice(1), call, show, json);
285
+ case "label":
286
+ return await labelCommand(argv.slice(1), call, show);
285
287
  case "shares":
286
288
  return await sharesCommand(argv.slice(1), call, show);
287
289
  case "lyrics":
@@ -438,6 +440,113 @@ async function aiCommand(argv, call, show, json) {
438
440
  return 2;
439
441
  }
440
442
  }
443
+ /** `nixamp station label ...`: AI artists who put out an album a month (ai-label.ts). */
444
+ async function labelCommand(argv, call, show) {
445
+ const [sub, key] = argv;
446
+ const when = (at) => at === -1 ? "never (off)" : at === 0 ? "now" : new Date(at).toISOString().slice(0, 10);
447
+ const describe = (one) => {
448
+ const albums = one["albums"] ?? [];
449
+ return [
450
+ `${String(one["name"])} [${String(one["key"])}] ${String(one["genre"])}${one["enabled"] ? "" : " (off)"}`,
451
+ ...albums.map((album) => ` ${String(album["year"])} ${String(album["title"])} ${String(album["composed"])}/${String(album["songs"])} songs${album["doneAt"] ? "" : " (recording)"}`),
452
+ ` next album ${when(Number(one["nextAlbumAt"] ?? -1))}`,
453
+ ...(one["lastError"] ? [` problem ${String(one["lastError"])}`] : []),
454
+ ].join("\n");
455
+ };
456
+ switch (sub) {
457
+ case undefined:
458
+ case "status":
459
+ case "list":
460
+ return show(await call("GET", "/api/v1/stations/label"), (body) => {
461
+ const settings = (body["settings"] ?? {});
462
+ const artists = body["artists"] ?? [];
463
+ const roster = body["roster"] ?? [];
464
+ return [
465
+ `holder ${String(settings["holder"])}${settings["enabled"] ? "" : " (off)"}`,
466
+ `files ${String(body["root"])}/<genre>/<artist>/<year>-<album>/`,
467
+ `composer ${String(body["composer"] ?? "none")}; words by ${String(body["writer"] ?? "nobody (no model key)")}`,
468
+ `this month ${String(body["secondsThisMonth"])} of ${Number(settings["monthlySeconds"]) === 0 ? "unlimited" : String(settings["monthlySeconds"])} s${body["busy"] ? `; recording ${String(body["busy"])}` : ""}`,
469
+ `albums ${String(settings["songsPerAlbum"])} songs of ${String(settings["songSeconds"])} s; ${String(settings["seedAlbums"])} to start, then one every ${String(settings["everyDays"])} days`,
470
+ ...(body["waiting"] ? [`waiting ${String(body["waiting"])}`] : []),
471
+ "",
472
+ ...(artists.length ? artists.map(describe) : ["Nobody signed yet. nixamp station label sign --roster"]),
473
+ "",
474
+ `roster ${roster.map((one) => `${String(one["key"])}${one["signed"] ? " (signed)" : ""}`).join(", ")}`,
475
+ ].join("\n");
476
+ });
477
+ case "sign": {
478
+ const body = has(argv, "--roster") ? { roster: true } : {
479
+ ...(flag(argv, "--preset") ? { preset: flag(argv, "--preset") } : {}),
480
+ ...(flag(argv, "--name") ? { name: flag(argv, "--name") } : {}),
481
+ ...(flag(argv, "--genre") ? { genre: flag(argv, "--genre") } : {}),
482
+ ...(flag(argv, "--sound") ? { sound: flag(argv, "--sound") } : {}),
483
+ ...(flag(argv, "--voice") ? { voice: flag(argv, "--voice") } : {}),
484
+ ...(flag(argv, "--bio") ? { bio: flag(argv, "--bio") } : {}),
485
+ ...(flag(argv, "--themes") ? { themes: flag(argv, "--themes") } : {}),
486
+ };
487
+ return show(await call("POST", "/api/v1/stations/label/artists", body), (answer) => {
488
+ const signed = answer["artists"] ?? (answer["artist"] ? [answer["artist"]] : []);
489
+ return signed.length ? `Signed ${signed.map((one) => `${String(one["name"])} (${String(one["genre"])})`).join(", ")}. Their first albums are planned and recorded from here.` : "Everybody on the roster is already signed.";
490
+ });
491
+ }
492
+ case "album": {
493
+ if (!key) {
494
+ console.error("nixamp station label album needs the artist's key (nixamp station label)");
495
+ return 2;
496
+ }
497
+ return show(await call("POST", `/api/v1/stations/label/artists/${key}/album`, {}), (answer) => {
498
+ const album = answer["album"];
499
+ return `Planned "${String(album["title"])}" (${(album["songs"] ?? []).length} songs); recording now, a song at a time.`;
500
+ });
501
+ }
502
+ case "set": {
503
+ if (!key) {
504
+ console.error("nixamp station label set needs the artist's key");
505
+ return 2;
506
+ }
507
+ const body = {
508
+ ...(flag(argv, "--sound") ? { sound: flag(argv, "--sound") } : {}),
509
+ ...(flag(argv, "--voice") ? { voice: flag(argv, "--voice") } : {}),
510
+ ...(flag(argv, "--bio") ? { bio: flag(argv, "--bio") } : {}),
511
+ ...(flag(argv, "--themes") ? { themes: flag(argv, "--themes") } : {}),
512
+ ...(has(argv, "--off") ? { enabled: false } : has(argv, "--on") ? { enabled: true } : {}),
513
+ };
514
+ return show(await call("PATCH", `/api/v1/stations/label/artists/${key}`, body), (answer) => `${String(answer["artist"]["name"])} updated.`);
515
+ }
516
+ case "drop": {
517
+ if (!key) {
518
+ console.error("nixamp station label drop needs the artist's key");
519
+ return 2;
520
+ }
521
+ const files = has(argv, "--files");
522
+ return show(await call("DELETE", `/api/v1/stations/label/artists/${key}${files ? "?files=1" : ""}`), () => files ? "Dropped, with their music." : "Dropped. Their albums stay on the air; nothing new will be recorded.");
523
+ }
524
+ case "config": {
525
+ const number = (name) => (flag(argv, name) === undefined ? undefined : Number(flag(argv, name)));
526
+ const body = Object.fromEntries(Object.entries({
527
+ holder: flag(argv, "--holder"),
528
+ monthlySeconds: number("--budget"),
529
+ everyDays: number("--every-days"),
530
+ seedAlbums: number("--seed-albums"),
531
+ songsPerAlbum: number("--songs"),
532
+ songSeconds: number("--seconds"),
533
+ enabled: has(argv, "--off") ? false : has(argv, "--on") ? true : undefined,
534
+ publish: has(argv, "--unlisted") ? false : has(argv, "--public") ? true : undefined,
535
+ }).filter(([, value]) => value !== undefined));
536
+ return show(await call("POST", "/api/v1/stations/label", body), (answer) => {
537
+ const settings = answer["settings"];
538
+ return `Label: ${String(settings["holder"])}, ${String(settings["songsPerAlbum"])} songs of ${String(settings["songSeconds"])} s an album, every ${String(settings["everyDays"])} days, at most ${Number(settings["monthlySeconds"]) === 0 ? "unlimited" : `${String(settings["monthlySeconds"])} s`} a month${settings["enabled"] ? "" : " (off)"}.`;
539
+ });
540
+ }
541
+ case "run":
542
+ return show(await call("POST", "/api/v1/stations/label/run", {}), (body) => body["waiting"] ? `Nothing to record now: ${String(body["waiting"])}.` : "Recording whatever is due.");
543
+ case "resume":
544
+ return show(await call("POST", "/api/v1/stations/label", { resume: true }), () => "Recording again.");
545
+ default:
546
+ console.error(`nixamp station label: no command "${sub}".\n\n${USAGE}`);
547
+ return 2;
548
+ }
549
+ }
441
550
  /** `nixamp station lyrics ...`: our own songs' words, published back to nichedb.dev. */
442
551
  async function lyricsCommand(argv, call, show, json) {
443
552
  const [sub, id] = argv;
@@ -27,7 +27,7 @@ export interface GuideEntry {
27
27
  * gateway that cannot run its script against this server (the Moshpit's
28
28
  * /n/<name>), and for a crawler. Everything from a station is escaped.
29
29
  */
30
- export declare function renderGuide(entries: GuideEntry[], apiBase?: string, ads?: GuideAds): string;
30
+ export declare function renderGuide(entries: GuideEntry[], apiBase?: string, ads?: GuideAds, labelHref?: string): string;
31
31
  export declare const GUIDE_HTML: string;
32
32
  /**
33
33
  * The guide's policy. With crawlproof configured (guideAds below) its tracker
@@ -31,7 +31,7 @@ button.report:hover{color:var(--warn)}
31
31
  #share .or{color:var(--muted);font-size:.85rem;margin:8px 0 0}
32
32
  #status{margin-top:10px;font-size:.9rem;min-height:1.4em}
33
33
  footer{margin-top:40px;color:var(--muted);font-size:.85rem}footer a{color:inherit}
34
- .ad-unit{margin:28px auto 0;display:flex;justify-content:center}.ad-unit iframe{border:0;display:block;background:transparent}
34
+ .ad-unit{margin:0 auto 16px;display:flex;justify-content:center}.ad-unit iframe{border:0;display:block;background:transparent}
35
35
  .ad-banner_300x250 iframe{width:300px;height:250px;max-width:100%}.ad-text_link iframe{width:100%;height:40px}
36
36
  `;
37
37
  /**
@@ -150,7 +150,7 @@ const GROUPS = ["rooms", "vod", "ai", "shared", "radio", "tv"];
150
150
  * gateway that cannot run its script against this server (the Moshpit's
151
151
  * /n/<name>), and for a crawler. Everything from a station is escaped.
152
152
  */
153
- export function renderGuide(entries, apiBase = "", ads = {}) {
153
+ export function renderGuide(entries, apiBase = "", ads = {}, labelHref = "") {
154
154
  const groupOf = (one) => one.group ?? (one.kind === "video" ? "tv" : "radio");
155
155
  const cards = (group) => entries
156
156
  .filter((one) => groupOf(one) === group && /^https?:\/\//.test(one.page))
@@ -166,11 +166,16 @@ export function renderGuide(entries, apiBase = "", ads = {}) {
166
166
  if (ads.site)
167
167
  page = page.replace("</head>", `<script data-site="${escape(ads.site)}" src="${CRAWLPROOF}/stats.js" async></script>\n</head>`);
168
168
  if (ads.slot) {
169
- const format = ads.format ?? "banner_300x250";
169
+ // One line by default: a 300x250 box above the stations pushed the guide itself off the first screen.
170
+ const format = ads.format ?? "text_link";
170
171
  const slot = ads.slot;
171
172
  // The one ad goes at the top of the content, never against the footer.
172
173
  page = page.replace('<p class="lead">Radio and TV, on the air now. Pick one and press play.</p>\n', (lead) => `${lead} <div class="ad-unit ad-${format}"><iframe src="${CRAWLPROOF}/api/ads/frame?slot=${encodeURIComponent(slot)}&amp;format=${format}" title="Advertisement" loading="lazy" referrerpolicy="strict-origin-when-cross-origin" sandbox="allow-scripts allow-popups allow-popups-to-escape-sandbox allow-top-navigation-by-user-activation" scrolling="no"></iframe></div>\n`);
173
174
  }
175
+ // The AI label's artists and albums, under the AI stations they play on.
176
+ if (/^\/[a-z]+$/.test(labelHref)) {
177
+ page = page.replace('<div id="ai" class="grid" aria-live="polite">', `<p class="lead"><a href="${labelHref}">The AI label: every artist, album and song, with the words →</a></p>\n <div id="ai" class="grid" aria-live="polite">`);
178
+ }
174
179
  const api = /^https:\/\/[^\s"<>]+$/.test(apiBase) ? apiBase.replace(/\/+$/, "") : "";
175
180
  return (page.replace('<p id="empty" class="empty">', entries.length ? '<p id="empty" class="empty" hidden>' : '<p id="empty" class="empty">')).replace("<body>", api ? `<body data-api="${escape(api)}">` : "<body>");
176
181
  }
@@ -26,15 +26,26 @@ export declare const STATION_TOOLS: ({
26
26
  breaksEvery?: undefined;
27
27
  start?: undefined;
28
28
  off?: undefined;
29
- key?: undefined;
30
- preset?: undefined;
31
29
  style?: undefined;
32
30
  description?: undefined;
33
31
  vocals?: undefined;
34
32
  trackSeconds?: undefined;
35
33
  perDay?: undefined;
34
+ key?: undefined;
35
+ preset?: undefined;
36
+ roster?: undefined;
37
+ genre?: undefined;
38
+ sound?: undefined;
39
+ voice?: undefined;
40
+ bio?: undefined;
41
+ themes?: undefined;
36
42
  enabled?: undefined;
43
+ holder?: undefined;
37
44
  monthlySeconds?: undefined;
45
+ everyDays?: undefined;
46
+ seedAlbums?: undefined;
47
+ songsPerAlbum?: undefined;
48
+ songSeconds?: undefined;
38
49
  state?: undefined;
39
50
  reason?: undefined;
40
51
  open?: undefined;
@@ -86,15 +97,26 @@ export declare const STATION_TOOLS: ({
86
97
  breaksEvery?: undefined;
87
98
  start?: undefined;
88
99
  off?: undefined;
89
- key?: undefined;
90
- preset?: undefined;
91
100
  style?: undefined;
92
101
  description?: undefined;
93
102
  vocals?: undefined;
94
103
  trackSeconds?: undefined;
95
104
  perDay?: undefined;
105
+ key?: undefined;
106
+ preset?: undefined;
107
+ roster?: undefined;
108
+ genre?: undefined;
109
+ sound?: undefined;
110
+ voice?: undefined;
111
+ bio?: undefined;
112
+ themes?: undefined;
96
113
  enabled?: undefined;
114
+ holder?: undefined;
97
115
  monthlySeconds?: undefined;
116
+ everyDays?: undefined;
117
+ seedAlbums?: undefined;
118
+ songsPerAlbum?: undefined;
119
+ songSeconds?: undefined;
98
120
  state?: undefined;
99
121
  reason?: undefined;
100
122
  open?: undefined;
@@ -170,14 +192,25 @@ export declare const STATION_TOOLS: ({
170
192
  breaksEvery?: undefined;
171
193
  start?: undefined;
172
194
  off?: undefined;
173
- key?: undefined;
174
- preset?: undefined;
175
195
  style?: undefined;
176
196
  vocals?: undefined;
177
197
  trackSeconds?: undefined;
178
198
  perDay?: undefined;
199
+ key?: undefined;
200
+ preset?: undefined;
201
+ roster?: undefined;
202
+ genre?: undefined;
203
+ sound?: undefined;
204
+ voice?: undefined;
205
+ bio?: undefined;
206
+ themes?: undefined;
179
207
  enabled?: undefined;
208
+ holder?: undefined;
180
209
  monthlySeconds?: undefined;
210
+ everyDays?: undefined;
211
+ seedAlbums?: undefined;
212
+ songsPerAlbum?: undefined;
213
+ songSeconds?: undefined;
181
214
  state?: undefined;
182
215
  reason?: undefined;
183
216
  open?: undefined;
@@ -250,14 +283,25 @@ export declare const STATION_TOOLS: ({
250
283
  breaksEvery?: undefined;
251
284
  start?: undefined;
252
285
  off?: undefined;
253
- key?: undefined;
254
- preset?: undefined;
255
286
  style?: undefined;
256
287
  vocals?: undefined;
257
288
  trackSeconds?: undefined;
258
289
  perDay?: undefined;
290
+ key?: undefined;
291
+ preset?: undefined;
292
+ roster?: undefined;
293
+ genre?: undefined;
294
+ sound?: undefined;
295
+ voice?: undefined;
296
+ bio?: undefined;
297
+ themes?: undefined;
259
298
  enabled?: undefined;
299
+ holder?: undefined;
260
300
  monthlySeconds?: undefined;
301
+ everyDays?: undefined;
302
+ seedAlbums?: undefined;
303
+ songsPerAlbum?: undefined;
304
+ songSeconds?: undefined;
261
305
  state?: undefined;
262
306
  reason?: undefined;
263
307
  open?: undefined;
@@ -311,15 +355,26 @@ export declare const STATION_TOOLS: ({
311
355
  breaksEvery?: undefined;
312
356
  start?: undefined;
313
357
  off?: undefined;
314
- key?: undefined;
315
- preset?: undefined;
316
358
  style?: undefined;
317
359
  description?: undefined;
318
360
  vocals?: undefined;
319
361
  trackSeconds?: undefined;
320
362
  perDay?: undefined;
363
+ key?: undefined;
364
+ preset?: undefined;
365
+ roster?: undefined;
366
+ genre?: undefined;
367
+ sound?: undefined;
368
+ voice?: undefined;
369
+ bio?: undefined;
370
+ themes?: undefined;
321
371
  enabled?: undefined;
372
+ holder?: undefined;
322
373
  monthlySeconds?: undefined;
374
+ everyDays?: undefined;
375
+ seedAlbums?: undefined;
376
+ songsPerAlbum?: undefined;
377
+ songSeconds?: undefined;
323
378
  state?: undefined;
324
379
  reason?: undefined;
325
380
  open?: undefined;
@@ -380,15 +435,26 @@ export declare const STATION_TOOLS: ({
380
435
  breaksEvery?: undefined;
381
436
  start?: undefined;
382
437
  off?: undefined;
383
- key?: undefined;
384
- preset?: undefined;
385
438
  style?: undefined;
386
439
  description?: undefined;
387
440
  vocals?: undefined;
388
441
  trackSeconds?: undefined;
389
442
  perDay?: undefined;
443
+ key?: undefined;
444
+ preset?: undefined;
445
+ roster?: undefined;
446
+ genre?: undefined;
447
+ sound?: undefined;
448
+ voice?: undefined;
449
+ bio?: undefined;
450
+ themes?: undefined;
390
451
  enabled?: undefined;
452
+ holder?: undefined;
391
453
  monthlySeconds?: undefined;
454
+ everyDays?: undefined;
455
+ seedAlbums?: undefined;
456
+ songsPerAlbum?: undefined;
457
+ songSeconds?: undefined;
392
458
  state?: undefined;
393
459
  reason?: undefined;
394
460
  open?: undefined;
@@ -449,15 +515,26 @@ export declare const STATION_TOOLS: ({
449
515
  off: {
450
516
  type: string;
451
517
  };
452
- key?: undefined;
453
- preset?: undefined;
454
518
  style?: undefined;
455
519
  description?: undefined;
456
520
  vocals?: undefined;
457
521
  trackSeconds?: undefined;
458
522
  perDay?: undefined;
523
+ key?: undefined;
524
+ preset?: undefined;
525
+ roster?: undefined;
526
+ genre?: undefined;
527
+ sound?: undefined;
528
+ voice?: undefined;
529
+ bio?: undefined;
530
+ themes?: undefined;
459
531
  enabled?: undefined;
532
+ holder?: undefined;
460
533
  monthlySeconds?: undefined;
534
+ everyDays?: undefined;
535
+ seedAlbums?: undefined;
536
+ songsPerAlbum?: undefined;
537
+ songSeconds?: undefined;
461
538
  state?: undefined;
462
539
  reason?: undefined;
463
540
  open?: undefined;
@@ -544,6 +621,17 @@ export declare const STATION_TOOLS: ({
544
621
  monthlySeconds: {
545
622
  type: string;
546
623
  };
624
+ roster?: undefined;
625
+ genre?: undefined;
626
+ sound?: undefined;
627
+ voice?: undefined;
628
+ bio?: undefined;
629
+ themes?: undefined;
630
+ holder?: undefined;
631
+ everyDays?: undefined;
632
+ seedAlbums?: undefined;
633
+ songsPerAlbum?: undefined;
634
+ songSeconds?: undefined;
547
635
  state?: undefined;
548
636
  reason?: undefined;
549
637
  open?: undefined;
@@ -584,15 +672,132 @@ export declare const STATION_TOOLS: ({
584
672
  breaksEvery?: undefined;
585
673
  start?: undefined;
586
674
  off?: undefined;
587
- key?: undefined;
588
- preset?: undefined;
589
675
  style?: undefined;
590
676
  description?: undefined;
591
677
  vocals?: undefined;
592
678
  trackSeconds?: undefined;
593
679
  perDay?: undefined;
680
+ server: {
681
+ type: "string";
682
+ description: string;
683
+ };
684
+ action: {
685
+ type: string;
686
+ enum: string[];
687
+ };
688
+ key: {
689
+ type: "string";
690
+ description: string;
691
+ };
692
+ preset: {
693
+ readonly type: "string";
694
+ };
695
+ roster: {
696
+ type: string;
697
+ };
698
+ name: {
699
+ readonly type: "string";
700
+ };
701
+ genre: {
702
+ readonly type: "string";
703
+ };
704
+ sound: {
705
+ type: "string";
706
+ description: string;
707
+ };
708
+ voice: {
709
+ readonly type: "string";
710
+ };
711
+ bio: {
712
+ readonly type: "string";
713
+ };
714
+ themes: {
715
+ readonly type: "string";
716
+ };
717
+ enabled: {
718
+ type: string;
719
+ };
720
+ publish: {
721
+ type: string;
722
+ };
723
+ holder: {
724
+ readonly type: "string";
725
+ };
726
+ monthlySeconds: {
727
+ type: string;
728
+ };
729
+ everyDays: {
730
+ type: string;
731
+ };
732
+ seedAlbums: {
733
+ type: string;
734
+ };
735
+ songsPerAlbum: {
736
+ type: string;
737
+ };
738
+ songSeconds: {
739
+ type: string;
740
+ };
741
+ state?: undefined;
742
+ reason?: undefined;
743
+ open?: undefined;
744
+ moderation?: undefined;
745
+ maxMb?: undefined;
746
+ perHour?: undefined;
747
+ artists?: undefined;
748
+ stations?: undefined;
749
+ support?: undefined;
750
+ label?: undefined;
751
+ license?: undefined;
752
+ id?: undefined;
753
+ private?: undefined;
754
+ };
755
+ required?: undefined;
756
+ };
757
+ } | {
758
+ name: string;
759
+ description: string;
760
+ inputSchema: {
761
+ type: string;
762
+ properties: {
763
+ playlist?: undefined;
764
+ idempotencyKey?: undefined;
765
+ folder?: undefined;
766
+ items?: undefined;
767
+ shuffle?: undefined;
768
+ repeat?: undefined;
769
+ bitrate?: undefined;
770
+ visibility?: undefined;
771
+ listed?: undefined;
772
+ basis?: undefined;
773
+ note?: undefined;
774
+ video?: undefined;
775
+ minItems?: undefined;
776
+ minFilms?: undefined;
777
+ maxVideoRunning?: undefined;
778
+ breaksEvery?: undefined;
779
+ start?: undefined;
780
+ off?: undefined;
781
+ style?: undefined;
782
+ description?: undefined;
783
+ vocals?: undefined;
784
+ trackSeconds?: undefined;
785
+ perDay?: undefined;
786
+ key?: undefined;
787
+ preset?: undefined;
788
+ roster?: undefined;
789
+ genre?: undefined;
790
+ sound?: undefined;
791
+ voice?: undefined;
792
+ bio?: undefined;
793
+ themes?: undefined;
594
794
  enabled?: undefined;
795
+ holder?: undefined;
595
796
  monthlySeconds?: undefined;
797
+ everyDays?: undefined;
798
+ seedAlbums?: undefined;
799
+ songsPerAlbum?: undefined;
800
+ songSeconds?: undefined;
596
801
  server: {
597
802
  type: "string";
598
803
  description: string;
@@ -662,15 +867,26 @@ export declare const STATION_TOOLS: ({
662
867
  breaksEvery?: undefined;
663
868
  start?: undefined;
664
869
  off?: undefined;
665
- key?: undefined;
666
- preset?: undefined;
667
870
  style?: undefined;
668
871
  description?: undefined;
669
872
  vocals?: undefined;
670
873
  trackSeconds?: undefined;
671
874
  perDay?: undefined;
875
+ key?: undefined;
876
+ preset?: undefined;
877
+ roster?: undefined;
878
+ genre?: undefined;
879
+ sound?: undefined;
880
+ voice?: undefined;
881
+ bio?: undefined;
882
+ themes?: undefined;
672
883
  enabled?: undefined;
884
+ holder?: undefined;
673
885
  monthlySeconds?: undefined;
886
+ everyDays?: undefined;
887
+ seedAlbums?: undefined;
888
+ songsPerAlbum?: undefined;
889
+ songSeconds?: undefined;
674
890
  state?: undefined;
675
891
  reason?: undefined;
676
892
  open?: undefined;
@@ -742,15 +958,26 @@ export declare const STATION_TOOLS: ({
742
958
  breaksEvery?: undefined;
743
959
  start?: undefined;
744
960
  off?: undefined;
745
- key?: undefined;
746
- preset?: undefined;
747
961
  style?: undefined;
748
962
  description?: undefined;
749
963
  vocals?: undefined;
750
964
  trackSeconds?: undefined;
751
965
  perDay?: undefined;
966
+ key?: undefined;
967
+ preset?: undefined;
968
+ roster?: undefined;
969
+ genre?: undefined;
970
+ sound?: undefined;
971
+ voice?: undefined;
972
+ bio?: undefined;
973
+ themes?: undefined;
752
974
  enabled?: undefined;
975
+ holder?: undefined;
753
976
  monthlySeconds?: undefined;
977
+ everyDays?: undefined;
978
+ seedAlbums?: undefined;
979
+ songsPerAlbum?: undefined;
980
+ songSeconds?: undefined;
754
981
  state?: undefined;
755
982
  reason?: undefined;
756
983
  open?: undefined;
@@ -105,6 +105,23 @@ export const STATION_TOOLS = [
105
105
  },
106
106
  },
107
107
  },
108
+ {
109
+ name: "station_label",
110
+ description: "The AI label: AI artists (a name, genre, sound and voice) who release albums on their own, written and sung in English, filed as <library>/ai/<genre>/<artist>/<year>-<album>/001. <title>.mp3 with .lyrics.txt, a public station per genre, and the catalogue at /label. action status (default), sign (preset from the roster, roster: true for all of it, or name + genre + sound), album (plan the artist's next album now), update, drop, configure (holder, monthlySeconds, everyDays, seedAlbums, songsPerAlbum, songSeconds, enabled, publish), run, resume.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ server: SERVER,
115
+ action: { type: "string", enum: ["status", "sign", "album", "update", "drop", "configure", "run", "resume"] },
116
+ key: { ...STRING, description: "The artist's key (10 hex characters), from status." },
117
+ preset: STRING, roster: { type: "boolean" }, name: STRING, genre: STRING,
118
+ sound: { ...STRING, description: "The music in words: instruments, tempo, mood. Name no real artist." },
119
+ voice: STRING, bio: STRING, themes: STRING, enabled: { type: "boolean" }, publish: { type: "boolean" }, holder: STRING,
120
+ monthlySeconds: { type: "integer" }, everyDays: { type: "integer" }, seedAlbums: { type: "integer" },
121
+ songsPerAlbum: { type: "integer" }, songSeconds: { type: "integer" },
122
+ },
123
+ },
124
+ },
108
125
  {
109
126
  name: "station_shares",
110
127
  description: "Listener shares: anybody may share an MP3 link or file from the public guide; AI files it under a genre and holds anything that looks like a commercial release. action status (default; waiting shares), list (state), configure (open, moderation ai|owner, publish, maxMb, perHour), approve, reject, delete.",
@@ -215,6 +232,25 @@ export async function callStationTool(name, args, options = {}) {
215
232
  return say("action is status, create, generate, update, remove, budget or resume", true);
216
233
  return answer.ok ? say(JSON.stringify(answer.body, null, 2)) : refused(answer.body);
217
234
  }
235
+ if (name === "station_label") {
236
+ const action = String(args["action"] ?? "status");
237
+ const key = typeof args["key"] === "string" && /^[a-f0-9]{10}$/.test(args["key"]) ? args["key"] : "";
238
+ const answer = action === "status" ? await call("GET", "/api/v1/stations/label")
239
+ : action === "sign" ? await call("POST", "/api/v1/stations/label/artists", pick(["preset", "roster", "name", "genre", "sound", "voice", "bio", "themes", "enabled"]))
240
+ : action === "configure" ? await call("POST", "/api/v1/stations/label", pick(["holder", "monthlySeconds", "everyDays", "seedAlbums", "songsPerAlbum", "songSeconds", "enabled", "publish"]))
241
+ : action === "run" ? await call("POST", "/api/v1/stations/label/run", {})
242
+ : action === "resume" ? await call("POST", "/api/v1/stations/label", { resume: true })
243
+ : !key ? null
244
+ : action === "album" ? await call("POST", `/api/v1/stations/label/artists/${key}/album`, {})
245
+ : action === "update" ? await call("PATCH", `/api/v1/stations/label/artists/${key}`, pick(["sound", "voice", "bio", "themes", "enabled"]))
246
+ : action === "drop" ? await call("DELETE", `/api/v1/stations/label/artists/${key}`)
247
+ : undefined;
248
+ if (answer === null)
249
+ return say("Which artist? Pass its key from action status.", true);
250
+ if (answer === undefined)
251
+ return say("action is status, sign, album, update, drop, configure, run or resume", true);
252
+ return answer.ok ? say(JSON.stringify(answer.body, null, 2)) : refused(answer.body);
253
+ }
218
254
  if (name === "station_holders") {
219
255
  const action = String(args["action"] ?? "list");
220
256
  const answer = action === "list" ? await call("GET", "/api/v1/stations/holders")
@@ -12,7 +12,7 @@
12
12
  *
13
13
  * NIXAMP_STATION_MAX_LISTENERS per station (default 200)
14
14
  * NIXAMP_MAX_LISTENERS across every station (default 500)
15
- * NIXAMP_MAX_RUNNING_STATIONS stations on the air at once (default 20)
15
+ * NIXAMP_MAX_RUNNING_STATIONS stations on the air at once (default 1000)
16
16
  *
17
17
  * The same names, lower-cased without the prefix, can be kept in config.json
18
18
  * under "limits" (e.g. {"limits": {"runningStations": 30}}); the environment
@@ -43,7 +43,7 @@ export function limitsFromEnv(env = process.env, saved = {}) {
43
43
  return {
44
44
  stationListeners: whole("NIXAMP_STATION_MAX_LISTENERS", "stationListeners", 200),
45
45
  totalListeners: whole("NIXAMP_MAX_LISTENERS", "totalListeners", 500),
46
- runningStations: whole("NIXAMP_MAX_RUNNING_STATIONS", "runningStations", 20),
46
+ runningStations: whole("NIXAMP_MAX_RUNNING_STATIONS", "runningStations", 1000),
47
47
  monthlyBytes: Math.round(Number(gb ?? 0) * 1024 ** 3) || 0,
48
48
  };
49
49
  }
@@ -1,2 +1,2 @@
1
1
  /** `nixamp station` usage, apart so `nixamp help station` does not load the client. */
2
- export declare const USAGE = "nixamp station \u2014 make a collection into a station\n\n nixamp station list\n nixamp station create --name NAME (--folder PATH | --playlist ID | --items 0,1,2) [--shuffle] [--no-repeat] [--bitrate 128] [--breaks 600 (the default; 0 for none)]\n nixamp station show ID\n nixamp station status ID\n nixamp station start ID\n nixamp station stop ID\n nixamp station skip ID\n nixamp station breaks ID SECONDS a spot between entries every SECONDS (120..86400; --off for none)\n nixamp station retry ID try every left-out file again\n nixamp station publish ID --basis owned|permission|licensed|public-domain [--note TEXT] [--unlisted] [--no-list]\n nixamp station unpublish ID [--private]\n nixamp station link ID [--rotate]\n nixamp station delete ID\n nixamp station guide-host NAME [--remove]\n NAME's front page is the public station guide (e.g. a Moshpit name)\n nixamp station guide-ads [--site ID] [--slot ID] [--format banner_300x250|text_link] [--off]\n crawlproof analytics (project ID) and an ad unit (slot ID) on the guide\n nixamp station guide-peer URL [--remove]\n the guide also lists the public, listed stations of the server at URL\n nixamp station auto [--no-video] [--min 10] [--films 3] [--tv 2] [--breaks 600] [--no-start]\n a station per genre, kept in step with the library (--off stops it)\n\n AI radio (music composed for a station, a few tracks a day):\n nixamp station ai what is composing, this month's seconds, the stations\n nixamp station ai create (--preset lofi|synthwave|ambient|chiptune|punk|jazz | --name NAME --style TEXT)\n [--vocals] [--seconds 120] [--per-day 2] [--unlisted] public on the guide unless --unlisted\n nixamp station ai generate KEY compose one now\n nixamp station ai set KEY [--style TEXT] [--per-day N] [--seconds N] [--off|--on]\n nixamp station ai remove KEY [--files]\n nixamp station ai budget SECONDS seconds of music a month across AI stations (0: no cap)\n nixamp station ai resume after the provider ran out of credit\n nixamp station ai key NAME ELEVENLABS_API_KEY | GEMINI_API_KEY | OPENAI_API_KEY | ANTHROPIC_API_KEY,\n NIXAMP_OPENAI_URL | NIXAMP_OPENAI_MODEL (an OpenAI-compatible stand-in), from stdin\n\n Listener shares (anybody shares an MP3 link or file from the guide; AI files and checks it):\n nixamp station shares the setting and what is waiting\n nixamp station shares on [--owner-moderation] [--unlisted] [--max-mb 60] [--per-hour 6]\n nixamp station shares off\n nixamp station shares list [--state held|approved|reported|rejected]\n nixamp station shares approve|reject|delete SHARE\n nixamp station shares trust ADDRESS [--remove] believe this proxy's X-Forwarded-For\n\n Lyrics (shown on the station page from each file's own tag, else nichedb.dev; ours published back):\n nixamp station lyrics the key, what was published, which stations may publish\n nixamp station lyrics publish ID publish a station's words now (public, listed, basis owned only)\n nixamp station lyrics key nichedb's key (NIXAMP_LYRICS_KEY), from stdin (--clear forgets it)\n\n Copyright holders (the now-playing card: who holds a track's copyright, and how to support them):\n nixamp station holder list\n nixamp station holder set NAME [--artist ARTIST]... [--station ID]... [--support https://...] [--label Subscribe] [--license TEXT]\n nixamp station holder remove ID\n\n --json machine-readable output\n --url, --key another server, as with `nixamp admin`\n\nA new station is private and stopped. Starting it does not publish it.";
2
+ export declare const USAGE = "nixamp station \u2014 make a collection into a station\n\n nixamp station list\n nixamp station create --name NAME (--folder PATH | --playlist ID | --items 0,1,2) [--shuffle] [--no-repeat] [--bitrate 128] [--breaks 600 (the default; 0 for none)]\n nixamp station show ID\n nixamp station status ID\n nixamp station start ID\n nixamp station stop ID\n nixamp station skip ID\n nixamp station breaks ID SECONDS a spot between entries every SECONDS (120..86400; --off for none)\n nixamp station retry ID try every left-out file again\n nixamp station publish ID --basis owned|permission|licensed|public-domain [--note TEXT] [--unlisted] [--no-list]\n nixamp station unpublish ID [--private]\n nixamp station link ID [--rotate]\n nixamp station delete ID\n nixamp station guide-host NAME [--remove]\n NAME's front page is the public station guide (e.g. a Moshpit name)\n nixamp station guide-ads [--site ID] [--slot ID] [--format banner_300x250|text_link] [--off]\n crawlproof analytics (project ID) and an ad unit (slot ID) on the guide\n nixamp station guide-peer URL [--remove]\n the guide also lists the public, listed stations of the server at URL\n nixamp station auto [--no-video] [--min 10] [--films 3] [--tv 2] [--breaks 600] [--no-start]\n a station per genre, kept in step with the library (--off stops it)\n\n AI radio (music composed for a station, a few tracks a day):\n nixamp station ai what is composing, this month's seconds, the stations\n nixamp station ai create (--preset lofi|synthwave|ambient|chiptune|punk|jazz | --name NAME --style TEXT)\n [--vocals] [--seconds 120] [--per-day 2] [--unlisted] public on the guide unless --unlisted\n nixamp station ai generate KEY compose one now\n nixamp station ai set KEY [--style TEXT] [--per-day N] [--seconds N] [--off|--on]\n nixamp station ai remove KEY [--files]\n nixamp station ai budget SECONDS seconds of music a month across AI stations (0: no cap)\n nixamp station ai resume after the provider ran out of credit\n nixamp station ai key NAME ELEVENLABS_API_KEY | GEMINI_API_KEY | OPENAI_API_KEY | ANTHROPIC_API_KEY,\n NIXAMP_OPENAI_URL | NIXAMP_OPENAI_MODEL (an OpenAI-compatible stand-in),\n NIXAMP_MUSIC_MODEL (music_v1|music_v2|music_v2_5), from stdin\n\n AI label (AI artists who release albums on their own; a station per genre; the catalogue at /label):\n nixamp station label artists, albums, this month's seconds, what it is waiting for\n nixamp station label sign --roster the first roster: velvet-ruckus, juno-kincaid, chrome-orchard, delta-wren,\n ashen-cartography, lumi-kestrel\n nixamp station label sign (--preset KEY | --name NAME --genre GENRE --sound TEXT [--voice TEXT] [--bio TEXT] [--themes TEXT])\n nixamp station label album KEY plan the artist's next album now and record it\n nixamp station label set KEY [--sound TEXT] [--voice TEXT] [--bio TEXT] [--themes TEXT] [--off|--on]\n nixamp station label drop KEY [--files]\n nixamp station label config [--holder \"Profullstack, Inc.\"] [--budget SECONDS] [--every-days 30] [--seed-albums 2]\n [--songs 8] [--seconds 150] [--on|--off] [--public|--unlisted]\n nixamp station label run record whatever is due now\n nixamp station label resume after the provider ran out of credit\n\n Listener shares (anybody shares an MP3 link or file from the guide; AI files and checks it):\n nixamp station shares the setting and what is waiting\n nixamp station shares on [--owner-moderation] [--unlisted] [--max-mb 60] [--per-hour 6]\n nixamp station shares off\n nixamp station shares list [--state held|approved|reported|rejected]\n nixamp station shares approve|reject|delete SHARE\n nixamp station shares trust ADDRESS [--remove] believe this proxy's X-Forwarded-For\n\n Lyrics (shown on the station page from each file's own tag, else nichedb.dev; ours published back):\n nixamp station lyrics the key, what was published, which stations may publish\n nixamp station lyrics publish ID publish a station's words now (public, listed, basis owned only)\n nixamp station lyrics key nichedb's key (NIXAMP_LYRICS_KEY), from stdin (--clear forgets it)\n\n Copyright holders (the now-playing card: who holds a track's copyright, and how to support them):\n nixamp station holder list\n nixamp station holder set NAME [--artist ARTIST]... [--station ID]... [--support https://...] [--label Subscribe] [--license TEXT]\n nixamp station holder remove ID\n\n --json machine-readable output\n --url, --key another server, as with `nixamp admin`\n\nA new station is private and stopped. Starting it does not publish it.";