nixamp 0.36.4 → 0.37.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 (59) hide show
  1. package/dist/ai-keys.d.ts +9 -0
  2. package/dist/ai-keys.js +46 -0
  3. package/dist/ai-radio.d.ts +148 -0
  4. package/dist/ai-radio.js +477 -0
  5. package/dist/ai-routes.d.ts +31 -0
  6. package/dist/ai-routes.js +191 -0
  7. package/dist/ai-text.d.ts +23 -0
  8. package/dist/ai-text.js +102 -0
  9. package/dist/library.d.ts +2 -0
  10. package/dist/library.js +1 -0
  11. package/dist/listener-shares.d.ts +136 -0
  12. package/dist/listener-shares.js +486 -0
  13. package/dist/server.d.ts +8 -0
  14. package/dist/server.js +62 -1
  15. package/dist/station-cli.js +166 -0
  16. package/dist/station-guide.d.ts +4 -1
  17. package/dist/station-guide.js +118 -21
  18. package/dist/station-mcp.d.ts +291 -20
  19. package/dist/station-mcp.js +66 -0
  20. package/dist/station-routes.d.ts +4 -0
  21. package/dist/station-routes.js +8 -1
  22. package/dist/station-usage.d.ts +1 -1
  23. package/dist/station-usage.js +19 -0
  24. package/package.json +1 -1
  25. package/src/ai-keys.ts +49 -0
  26. package/src/ai-radio.ts +536 -0
  27. package/src/ai-routes.ts +220 -0
  28. package/src/ai-text.ts +117 -0
  29. package/src/library.ts +3 -0
  30. package/src/listener-shares.ts +528 -0
  31. package/src/server.ts +69 -1
  32. package/src/station-cli.ts +153 -0
  33. package/src/station-guide.ts +122 -21
  34. package/src/station-mcp.ts +62 -0
  35. package/src/station-routes.ts +11 -2
  36. package/src/station-usage.ts +19 -0
  37. package/web/dist/assets/{ar-B9jc6q-w.js → ar-B2m5eyoV.js} +1 -1
  38. package/web/dist/assets/{cs-lU25OfSM.js → cs-Cc5pOkWw.js} +1 -1
  39. package/web/dist/assets/{da-CAgdgQoL.js → da-10XVgmQy.js} +1 -1
  40. package/web/dist/assets/{de-CiusGizF.js → de-DIBX68fo.js} +1 -1
  41. package/web/dist/assets/{es-DVxl-KYi.js → es-B5wXCbD_.js} +1 -1
  42. package/web/dist/assets/{fi-C1-78Aib.js → fi-CfqtJU25.js} +1 -1
  43. package/web/dist/assets/{fr-BcAXoFCf.js → fr-DVpDbDWM.js} +1 -1
  44. package/web/dist/assets/{hi-DXdG5DSQ.js → hi-iQgB7g2O.js} +1 -1
  45. package/web/dist/assets/{hls-3VKVEQE3-C3dxJvEy.js → hls-3VKVEQE3-Hp3aQLSm.js} +1 -1
  46. package/web/dist/assets/{hu-qrAMY3zu.js → hu-cL8uAJW0.js} +1 -1
  47. package/web/dist/assets/{id-Cgtr-WML.js → id-DAEQSrx8.js} +1 -1
  48. package/web/dist/assets/{it-kuAXXts8.js → it-DARHJ2-F.js} +1 -1
  49. package/web/dist/assets/{main-C5IsPzDk.js → main-Bdbkg8vi.js} +2 -2
  50. package/web/dist/assets/{mpegts-LO6RVLD6-CYjdm0vd.js → mpegts-LO6RVLD6-Be6JzNGd.js} +1 -1
  51. package/web/dist/assets/{mpegts-BWzEpcbk.js → mpegts-vNdAhc1w.js} +1 -1
  52. package/web/dist/assets/{nl-CEXpOYtY.js → nl-BNskTvYD.js} +1 -1
  53. package/web/dist/assets/{ru-v7b_XixQ.js → ru-PA8keJv_.js} +1 -1
  54. package/web/dist/assets/{sv-CJK5wtih.js → sv-sZu9UVEg.js} +1 -1
  55. package/web/dist/assets/{uk-qDSjg5zB.js → uk-FmHT0lvK.js} +1 -1
  56. package/web/dist/assets/{vi-kckrzXyJ.js → vi-BV81HbEM.js} +1 -1
  57. package/web/dist/assets/{zh-Cbq-3ikH.js → zh-CwzApCu7.js} +1 -1
  58. package/web/dist/index.html +1 -1
  59. package/web/dist/sw.js +22 -22
@@ -10,14 +10,11 @@ export declare const STATION_TOOLS: ({
10
10
  };
11
11
  playlist?: undefined;
12
12
  idempotencyKey?: undefined;
13
- name?: undefined;
14
- description?: undefined;
15
13
  folder?: undefined;
16
14
  items?: undefined;
17
15
  shuffle?: undefined;
18
16
  repeat?: undefined;
19
17
  bitrate?: undefined;
20
- action?: undefined;
21
18
  visibility?: undefined;
22
19
  listed?: undefined;
23
20
  basis?: undefined;
@@ -29,6 +26,24 @@ export declare const STATION_TOOLS: ({
29
26
  breaksEvery?: undefined;
30
27
  start?: undefined;
31
28
  off?: undefined;
29
+ key?: undefined;
30
+ preset?: undefined;
31
+ name?: undefined;
32
+ style?: undefined;
33
+ description?: undefined;
34
+ vocals?: undefined;
35
+ trackSeconds?: undefined;
36
+ perDay?: undefined;
37
+ enabled?: undefined;
38
+ monthlySeconds?: undefined;
39
+ action?: undefined;
40
+ state?: undefined;
41
+ reason?: undefined;
42
+ open?: undefined;
43
+ moderation?: undefined;
44
+ publish?: undefined;
45
+ maxMb?: undefined;
46
+ perHour?: undefined;
32
47
  id?: undefined;
33
48
  private?: undefined;
34
49
  };
@@ -50,14 +65,11 @@ export declare const STATION_TOOLS: ({
50
65
  };
51
66
  playlist?: undefined;
52
67
  idempotencyKey?: undefined;
53
- name?: undefined;
54
- description?: undefined;
55
68
  folder?: undefined;
56
69
  items?: undefined;
57
70
  shuffle?: undefined;
58
71
  repeat?: undefined;
59
72
  bitrate?: undefined;
60
- action?: undefined;
61
73
  visibility?: undefined;
62
74
  listed?: undefined;
63
75
  basis?: undefined;
@@ -69,6 +81,24 @@ export declare const STATION_TOOLS: ({
69
81
  breaksEvery?: undefined;
70
82
  start?: undefined;
71
83
  off?: undefined;
84
+ key?: undefined;
85
+ preset?: undefined;
86
+ name?: undefined;
87
+ style?: undefined;
88
+ description?: undefined;
89
+ vocals?: undefined;
90
+ trackSeconds?: undefined;
91
+ perDay?: undefined;
92
+ enabled?: undefined;
93
+ monthlySeconds?: undefined;
94
+ action?: undefined;
95
+ state?: undefined;
96
+ reason?: undefined;
97
+ open?: undefined;
98
+ moderation?: undefined;
99
+ publish?: undefined;
100
+ maxMb?: undefined;
101
+ perHour?: undefined;
72
102
  private?: undefined;
73
103
  };
74
104
  required: string[];
@@ -119,7 +149,6 @@ export declare const STATION_TOOLS: ({
119
149
  type: "string";
120
150
  description: string;
121
151
  };
122
- action?: undefined;
123
152
  visibility?: undefined;
124
153
  listed?: undefined;
125
154
  basis?: undefined;
@@ -131,6 +160,22 @@ export declare const STATION_TOOLS: ({
131
160
  breaksEvery?: undefined;
132
161
  start?: undefined;
133
162
  off?: undefined;
163
+ key?: undefined;
164
+ preset?: undefined;
165
+ style?: undefined;
166
+ vocals?: undefined;
167
+ trackSeconds?: undefined;
168
+ perDay?: undefined;
169
+ enabled?: undefined;
170
+ monthlySeconds?: undefined;
171
+ action?: undefined;
172
+ state?: undefined;
173
+ reason?: undefined;
174
+ open?: undefined;
175
+ moderation?: undefined;
176
+ publish?: undefined;
177
+ maxMb?: undefined;
178
+ perHour?: undefined;
134
179
  id?: undefined;
135
180
  private?: undefined;
136
181
  };
@@ -162,24 +207,23 @@ export declare const STATION_TOOLS: ({
162
207
  readonly type: "string";
163
208
  };
164
209
  items: {
165
- description?: undefined;
166
210
  type: string;
167
211
  items: {
168
212
  type: string;
169
213
  };
214
+ description?: undefined;
170
215
  };
171
216
  shuffle: {
172
217
  type: string;
173
218
  };
174
219
  repeat: {
175
- description?: undefined;
176
220
  type: string;
221
+ description?: undefined;
177
222
  };
178
223
  bitrate: {
179
- description?: undefined;
180
224
  type: string;
225
+ description?: undefined;
181
226
  };
182
- action?: undefined;
183
227
  visibility?: undefined;
184
228
  listed?: undefined;
185
229
  basis?: undefined;
@@ -191,6 +235,22 @@ export declare const STATION_TOOLS: ({
191
235
  breaksEvery?: undefined;
192
236
  start?: undefined;
193
237
  off?: undefined;
238
+ key?: undefined;
239
+ preset?: undefined;
240
+ style?: undefined;
241
+ vocals?: undefined;
242
+ trackSeconds?: undefined;
243
+ perDay?: undefined;
244
+ enabled?: undefined;
245
+ monthlySeconds?: undefined;
246
+ action?: undefined;
247
+ state?: undefined;
248
+ reason?: undefined;
249
+ open?: undefined;
250
+ moderation?: undefined;
251
+ publish?: undefined;
252
+ maxMb?: undefined;
253
+ perHour?: undefined;
194
254
  private?: undefined;
195
255
  };
196
256
  required: string[];
@@ -203,8 +263,6 @@ export declare const STATION_TOOLS: ({
203
263
  properties: {
204
264
  playlist?: undefined;
205
265
  idempotencyKey?: undefined;
206
- name?: undefined;
207
- description?: undefined;
208
266
  folder?: undefined;
209
267
  items?: undefined;
210
268
  shuffle?: undefined;
@@ -233,6 +291,23 @@ export declare const STATION_TOOLS: ({
233
291
  breaksEvery?: undefined;
234
292
  start?: undefined;
235
293
  off?: undefined;
294
+ key?: undefined;
295
+ preset?: undefined;
296
+ name?: undefined;
297
+ style?: undefined;
298
+ description?: undefined;
299
+ vocals?: undefined;
300
+ trackSeconds?: undefined;
301
+ perDay?: undefined;
302
+ enabled?: undefined;
303
+ monthlySeconds?: undefined;
304
+ state?: undefined;
305
+ reason?: undefined;
306
+ open?: undefined;
307
+ moderation?: undefined;
308
+ publish?: undefined;
309
+ maxMb?: undefined;
310
+ perHour?: undefined;
236
311
  private?: undefined;
237
312
  };
238
313
  required: string[];
@@ -245,14 +320,11 @@ export declare const STATION_TOOLS: ({
245
320
  properties: {
246
321
  playlist?: undefined;
247
322
  idempotencyKey?: undefined;
248
- name?: undefined;
249
- description?: undefined;
250
323
  folder?: undefined;
251
324
  items?: undefined;
252
325
  shuffle?: undefined;
253
326
  repeat?: undefined;
254
327
  bitrate?: undefined;
255
- action?: undefined;
256
328
  server: {
257
329
  type: "string";
258
330
  description: string;
@@ -283,6 +355,24 @@ export declare const STATION_TOOLS: ({
283
355
  breaksEvery?: undefined;
284
356
  start?: undefined;
285
357
  off?: undefined;
358
+ key?: undefined;
359
+ preset?: undefined;
360
+ name?: undefined;
361
+ style?: undefined;
362
+ description?: undefined;
363
+ vocals?: undefined;
364
+ trackSeconds?: undefined;
365
+ perDay?: undefined;
366
+ enabled?: undefined;
367
+ monthlySeconds?: undefined;
368
+ action?: undefined;
369
+ state?: undefined;
370
+ reason?: undefined;
371
+ open?: undefined;
372
+ moderation?: undefined;
373
+ publish?: undefined;
374
+ maxMb?: undefined;
375
+ perHour?: undefined;
286
376
  private?: undefined;
287
377
  };
288
378
  required: string[];
@@ -295,14 +385,11 @@ export declare const STATION_TOOLS: ({
295
385
  properties: {
296
386
  playlist?: undefined;
297
387
  idempotencyKey?: undefined;
298
- name?: undefined;
299
- description?: undefined;
300
388
  folder?: undefined;
301
389
  items?: undefined;
302
390
  shuffle?: undefined;
303
391
  repeat?: undefined;
304
392
  bitrate?: undefined;
305
- action?: undefined;
306
393
  visibility?: undefined;
307
394
  listed?: undefined;
308
395
  basis?: undefined;
@@ -332,6 +419,24 @@ export declare const STATION_TOOLS: ({
332
419
  off: {
333
420
  type: string;
334
421
  };
422
+ key?: undefined;
423
+ preset?: undefined;
424
+ name?: undefined;
425
+ style?: undefined;
426
+ description?: undefined;
427
+ vocals?: undefined;
428
+ trackSeconds?: undefined;
429
+ perDay?: undefined;
430
+ enabled?: undefined;
431
+ monthlySeconds?: undefined;
432
+ action?: undefined;
433
+ state?: undefined;
434
+ reason?: undefined;
435
+ open?: undefined;
436
+ moderation?: undefined;
437
+ publish?: undefined;
438
+ maxMb?: undefined;
439
+ perHour?: undefined;
335
440
  id?: undefined;
336
441
  private?: undefined;
337
442
  };
@@ -345,14 +450,162 @@ export declare const STATION_TOOLS: ({
345
450
  properties: {
346
451
  playlist?: undefined;
347
452
  idempotencyKey?: undefined;
453
+ folder?: undefined;
454
+ items?: undefined;
455
+ shuffle?: undefined;
456
+ repeat?: undefined;
457
+ bitrate?: undefined;
458
+ visibility?: undefined;
459
+ listed?: undefined;
460
+ basis?: undefined;
461
+ note?: undefined;
462
+ video?: undefined;
463
+ minItems?: undefined;
464
+ minFilms?: undefined;
465
+ maxVideoRunning?: undefined;
466
+ breaksEvery?: undefined;
467
+ start?: undefined;
468
+ off?: undefined;
469
+ server: {
470
+ type: "string";
471
+ description: string;
472
+ };
473
+ action: {
474
+ type: string;
475
+ enum: string[];
476
+ };
477
+ key: {
478
+ type: "string";
479
+ description: string;
480
+ };
481
+ preset: {
482
+ readonly type: "string";
483
+ };
484
+ name: {
485
+ readonly type: "string";
486
+ };
487
+ style: {
488
+ type: "string";
489
+ description: string;
490
+ };
491
+ description: {
492
+ readonly type: "string";
493
+ };
494
+ vocals: {
495
+ type: string;
496
+ };
497
+ trackSeconds: {
498
+ type: string;
499
+ };
500
+ perDay: {
501
+ type: string;
502
+ };
503
+ enabled: {
504
+ type: string;
505
+ };
506
+ publish: {
507
+ type: string;
508
+ };
509
+ monthlySeconds: {
510
+ type: string;
511
+ };
512
+ state?: undefined;
513
+ reason?: undefined;
514
+ open?: undefined;
515
+ moderation?: undefined;
516
+ maxMb?: undefined;
517
+ perHour?: undefined;
518
+ id?: undefined;
519
+ private?: undefined;
520
+ };
521
+ required?: undefined;
522
+ };
523
+ } | {
524
+ name: string;
525
+ description: string;
526
+ inputSchema: {
527
+ type: string;
528
+ properties: {
529
+ playlist?: undefined;
530
+ idempotencyKey?: undefined;
531
+ folder?: undefined;
532
+ items?: undefined;
533
+ shuffle?: undefined;
534
+ repeat?: undefined;
535
+ bitrate?: undefined;
536
+ visibility?: undefined;
537
+ listed?: undefined;
538
+ basis?: undefined;
539
+ note?: undefined;
540
+ video?: undefined;
541
+ minItems?: undefined;
542
+ minFilms?: undefined;
543
+ maxVideoRunning?: undefined;
544
+ breaksEvery?: undefined;
545
+ start?: undefined;
546
+ off?: undefined;
547
+ key?: undefined;
548
+ preset?: undefined;
348
549
  name?: undefined;
550
+ style?: undefined;
349
551
  description?: undefined;
552
+ vocals?: undefined;
553
+ trackSeconds?: undefined;
554
+ perDay?: undefined;
555
+ enabled?: undefined;
556
+ monthlySeconds?: undefined;
557
+ server: {
558
+ type: "string";
559
+ description: string;
560
+ };
561
+ action: {
562
+ type: string;
563
+ enum: string[];
564
+ };
565
+ id: {
566
+ type: "string";
567
+ description: string;
568
+ };
569
+ state: {
570
+ type: string;
571
+ enum: string[];
572
+ };
573
+ reason: {
574
+ readonly type: "string";
575
+ };
576
+ open: {
577
+ type: string;
578
+ };
579
+ moderation: {
580
+ type: string;
581
+ enum: string[];
582
+ };
583
+ publish: {
584
+ type: string;
585
+ };
586
+ maxMb: {
587
+ type: string;
588
+ };
589
+ perHour: {
590
+ type: string;
591
+ };
592
+ private?: undefined;
593
+ };
594
+ required?: undefined;
595
+ };
596
+ } | {
597
+ name: string;
598
+ description: string;
599
+ inputSchema: {
600
+ type: string;
601
+ properties: {
602
+ playlist?: undefined;
603
+ idempotencyKey?: undefined;
350
604
  folder?: undefined;
351
605
  items?: undefined;
352
606
  shuffle?: undefined;
353
607
  repeat?: undefined;
354
608
  bitrate?: undefined;
355
- action?: undefined;
356
609
  visibility?: undefined;
357
610
  listed?: undefined;
358
611
  basis?: undefined;
@@ -364,6 +617,24 @@ export declare const STATION_TOOLS: ({
364
617
  breaksEvery?: undefined;
365
618
  start?: undefined;
366
619
  off?: undefined;
620
+ key?: undefined;
621
+ preset?: undefined;
622
+ name?: undefined;
623
+ style?: undefined;
624
+ description?: undefined;
625
+ vocals?: undefined;
626
+ trackSeconds?: undefined;
627
+ perDay?: undefined;
628
+ enabled?: undefined;
629
+ monthlySeconds?: undefined;
630
+ action?: undefined;
631
+ state?: undefined;
632
+ reason?: undefined;
633
+ open?: undefined;
634
+ moderation?: undefined;
635
+ publish?: undefined;
636
+ maxMb?: undefined;
637
+ perHour?: undefined;
367
638
  server: {
368
639
  type: "string";
369
640
  description: string;
@@ -90,6 +90,37 @@ export const STATION_TOOLS = [
90
90
  },
91
91
  },
92
92
  },
93
+ {
94
+ name: "station_ai",
95
+ description: "AI radio: stations whose music is composed by AI (ElevenLabs Music or a local model) a few tracks a day, within a monthly budget of seconds. action status (default), create (preset lofi|synthwave|ambient|chiptune|punk|jazz, or name + style), generate (one track now), update, remove, budget (monthlySeconds), resume. A created station is public on the guide with basis owned unless publish is false.",
96
+ inputSchema: {
97
+ type: "object",
98
+ properties: {
99
+ server: SERVER,
100
+ action: { type: "string", enum: ["status", "create", "generate", "update", "remove", "budget", "resume"] },
101
+ key: { ...STRING, description: "The AI station's key (10 hex characters), from status." },
102
+ preset: STRING, name: STRING, style: { ...STRING, description: "The music in words: genre, instruments, mood, tempo. Name no artist." },
103
+ description: STRING, vocals: { type: "boolean" }, trackSeconds: { type: "integer" }, perDay: { type: "integer" },
104
+ enabled: { type: "boolean" }, publish: { type: "boolean" }, monthlySeconds: { type: "integer" },
105
+ },
106
+ },
107
+ },
108
+ {
109
+ name: "station_shares",
110
+ 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.",
111
+ inputSchema: {
112
+ type: "object",
113
+ properties: {
114
+ server: SERVER,
115
+ action: { type: "string", enum: ["status", "list", "configure", "approve", "reject", "delete"] },
116
+ id: { ...STRING, description: "A share's id (12 hex characters)." },
117
+ state: { type: "string", enum: ["held", "approved", "reported", "rejected", "failed", "withdrawn"] },
118
+ reason: STRING,
119
+ open: { type: "boolean" }, moderation: { type: "string", enum: ["ai", "owner"] }, publish: { type: "boolean" },
120
+ maxMb: { type: "integer" }, perHour: { type: "integer" },
121
+ },
122
+ },
123
+ },
93
124
  {
94
125
  name: "station_unpublish",
95
126
  description: "Take a station out of the directory, or make it private: its links stop working and listeners are disconnected.",
@@ -150,6 +181,41 @@ export async function callStationTool(name, args, options = {}) {
150
181
  const answer = await call("POST", "/api/v1/stations/auto", pick(["video", "minItems", "minFilms", "maxVideoRunning", "breaksEvery", "start", "off"]));
151
182
  return answer.ok ? say(JSON.stringify(answer.body["report"], null, 2)) : refused(answer.body);
152
183
  }
184
+ if (name === "station_ai") {
185
+ const action = String(args["action"] ?? "status");
186
+ const key = typeof args["key"] === "string" && /^[a-f0-9]{10}$/.test(args["key"]) ? args["key"] : "";
187
+ const fields = ["preset", "name", "style", "description", "vocals", "trackSeconds", "perDay", "enabled", "publish"];
188
+ const answer = action === "status" ? await call("GET", "/api/v1/stations/ai")
189
+ : action === "create" ? await call("POST", "/api/v1/stations/ai/stations", pick(fields))
190
+ : action === "budget" ? await call("POST", "/api/v1/stations/ai", pick(["monthlySeconds"]))
191
+ : action === "resume" ? await call("POST", "/api/v1/stations/ai", { resume: true })
192
+ : !key ? null
193
+ : action === "generate" ? await call("POST", `/api/v1/stations/ai/stations/${key}/generate`, {})
194
+ : action === "update" ? await call("PATCH", `/api/v1/stations/ai/stations/${key}`, pick(fields))
195
+ : action === "remove" ? await call("DELETE", `/api/v1/stations/ai/stations/${key}`)
196
+ : undefined;
197
+ if (answer === null)
198
+ return say("Which AI station? Pass its key from action status.", true);
199
+ if (answer === undefined)
200
+ return say("action is status, create, generate, update, remove, budget or resume", true);
201
+ return answer.ok ? say(JSON.stringify(answer.body, null, 2)) : refused(answer.body);
202
+ }
203
+ if (name === "station_shares") {
204
+ const action = String(args["action"] ?? "status");
205
+ const share = typeof args["id"] === "string" && /^[a-f0-9]{12}$/.test(args["id"]) ? args["id"] : "";
206
+ const answer = action === "status" ? await call("GET", "/api/v1/stations/shares?state=held")
207
+ : action === "list" ? await call("GET", `/api/v1/stations/shares${typeof args["state"] === "string" ? `?state=${encodeURIComponent(args["state"])}` : ""}`)
208
+ : action === "configure" ? await call("POST", "/api/v1/stations/shares", pick(["open", "moderation", "publish", "maxMb", "perHour"]))
209
+ : !share ? null
210
+ : action === "approve" || action === "reject" ? await call("POST", `/api/v1/stations/shares/${share}/${action}`, pick(["reason"]))
211
+ : action === "delete" ? await call("DELETE", `/api/v1/stations/shares/${share}`)
212
+ : undefined;
213
+ if (answer === null)
214
+ return say("Which share? Pass its id from action status or list.", true);
215
+ if (answer === undefined)
216
+ return say("action is status, list, configure, approve, reject or delete", true);
217
+ return answer.ok ? say(JSON.stringify(answer.body, null, 2)) : refused(answer.body);
218
+ }
153
219
  if (!/^[a-z0-9]{10}$/.test(id) && name !== "station_create")
154
220
  return say("Which station? Pass its id from station_list.", true);
155
221
  if (name === "station_get") {
@@ -70,10 +70,14 @@ declare function links(ctx: StationContext, station: Station, base?: string): {
70
70
  state: string;
71
71
  };
72
72
  export declare function publicView(ctx: StationContext, station: Station): PublicStation;
73
+ /** Where the guide puts a station: composed by AI, shared by listeners, or the owner's radio or TV. */
74
+ export type GuideGroup = "ai" | "shared" | "radio" | "tv";
75
+ export declare function guideGroup(station: Station): GuideGroup;
73
76
  /** What the public guide shows: public, listed stations, linked at this server's public address. */
74
77
  export declare function guideEntries(ctx: StationContext): Array<PublicStation & {
75
78
  page: string;
76
79
  m3u: string;
80
+ group: GuideGroup;
77
81
  }>;
78
82
  export declare function ownerView(ctx: StationContext, station: Station): OwnerStation & {
79
83
  links: ReturnType<typeof links>;
@@ -50,12 +50,19 @@ export function publicView(ctx, station) {
50
50
  listeners: ctx.runner.listeners(station.id),
51
51
  };
52
52
  }
53
+ export function guideGroup(station) {
54
+ if (station.auto.startsWith("ai:"))
55
+ return "ai";
56
+ if (station.auto.startsWith("share:"))
57
+ return "shared";
58
+ return station.video ? "tv" : "radio";
59
+ }
53
60
  /** What the public guide shows: public, listed stations, linked at this server's public address. */
54
61
  export function guideEntries(ctx) {
55
62
  const base = ctx.publicBase || ctx.base;
56
63
  return ctx.store.list()
57
64
  .filter((one) => one.visibility === "public" && one.listed)
58
- .map((one) => ({ ...publicView(ctx, one), page: links(ctx, one, base).page, m3u: links(ctx, one, base).m3u }));
65
+ .map((one) => ({ ...publicView(ctx, one), page: links(ctx, one, base).page, m3u: links(ctx, one, base).m3u, group: guideGroup(one) }));
59
66
  }
60
67
  export function ownerView(ctx, station) {
61
68
  const { failures, idempotencyKey: _key, ...rest } = station;
@@ -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]\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 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 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 --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]\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 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 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, 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 --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.";
@@ -18,6 +18,25 @@ export const USAGE = `nixamp station — make a collection into a station
18
18
  nixamp station auto [--no-video] [--min 10] [--films 3] [--tv 2] [--breaks 600] [--no-start]
19
19
  a station per genre, kept in step with the library (--off stops it)
20
20
 
21
+ AI radio (music composed for a station, a few tracks a day):
22
+ nixamp station ai what is composing, this month's seconds, the stations
23
+ nixamp station ai create (--preset lofi|synthwave|ambient|chiptune|punk|jazz | --name NAME --style TEXT)
24
+ [--vocals] [--seconds 120] [--per-day 2] [--unlisted] public on the guide unless --unlisted
25
+ nixamp station ai generate KEY compose one now
26
+ nixamp station ai set KEY [--style TEXT] [--per-day N] [--seconds N] [--off|--on]
27
+ nixamp station ai remove KEY [--files]
28
+ nixamp station ai budget SECONDS seconds of music a month across AI stations (0: no cap)
29
+ nixamp station ai resume after the provider ran out of credit
30
+ nixamp station ai key NAME ELEVENLABS_API_KEY | GEMINI_API_KEY | OPENAI_API_KEY | ANTHROPIC_API_KEY, from stdin
31
+
32
+ Listener shares (anybody shares an MP3 link or file from the guide; AI files and checks it):
33
+ nixamp station shares the setting and what is waiting
34
+ nixamp station shares on [--owner-moderation] [--unlisted] [--max-mb 60] [--per-hour 6]
35
+ nixamp station shares off
36
+ nixamp station shares list [--state held|approved|reported|rejected]
37
+ nixamp station shares approve|reject|delete SHARE
38
+ nixamp station shares trust ADDRESS [--remove] believe this proxy's X-Forwarded-For
39
+
21
40
  --json machine-readable output
22
41
  --url, --key another server, as with \`nixamp admin\`
23
42
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.36.4",
3
+ "version": "0.37.0",
4
4
  "description": "It really whips the terminal's ass. A Winamp-shaped audio player for your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/ai-keys.ts ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * The keys AI radio uses, and where they come from.
3
+ *
4
+ * The environment first, as everywhere else; then `ai.json` beside the
5
+ * server's own keys (0600), which `nixamp station ai key NAME` fills from
6
+ * stdin so a daemon restarted by `nixamp update` still has them. A key is
7
+ * never shown back: the owner view says which ones are set, not what they are.
8
+ */
9
+ import { mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
+ import { dirname, join } from "node:path";
11
+ import { stateDir } from "./daemon.ts";
12
+
13
+ export const AI_KEY_NAMES = ["ELEVENLABS_API_KEY", "GEMINI_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY"] as const;
14
+ export type AiKeyName = (typeof AI_KEY_NAMES)[number];
15
+
16
+ export function aiKeysFile(): string {
17
+ return join(stateDir(), "ai.json");
18
+ }
19
+
20
+ function saved(file = aiKeysFile()): Record<string, string> {
21
+ try {
22
+ const parsed = JSON.parse(readFileSync(file, "utf8")) as unknown;
23
+ if (typeof parsed !== "object" || parsed === null) return {};
24
+ return Object.fromEntries(Object.entries(parsed).filter((pair): pair is [string, string] => typeof pair[1] === "string"));
25
+ } catch {
26
+ return {};
27
+ }
28
+ }
29
+
30
+ /** The key, from the environment or ai.json; "" when neither has it. */
31
+ export function aiKey(name: AiKeyName, env: Record<string, string | undefined> = process.env, file = aiKeysFile()): string {
32
+ return (env[name] ?? "").trim() || (saved(file)[name] ?? "").trim();
33
+ }
34
+
35
+ /** Which keys are set, never their values. */
36
+ export function aiKeysSet(env: Record<string, string | undefined> = process.env, file = aiKeysFile()): Record<AiKeyName, boolean> {
37
+ return Object.fromEntries(AI_KEY_NAMES.map((name) => [name, aiKey(name, env, file) !== ""])) as Record<AiKeyName, boolean>;
38
+ }
39
+
40
+ /** Keep (or with "" forget) a key in ai.json. */
41
+ export function saveAiKey(name: AiKeyName, value: string, file = aiKeysFile()): void {
42
+ const next = saved(file);
43
+ if (value.trim()) next[name] = value.trim();
44
+ else delete next[name];
45
+ mkdirSync(dirname(file), { recursive: true });
46
+ const temporary = `${file}.${process.pid}.tmp`;
47
+ writeFileSync(temporary, `${JSON.stringify(next, null, 2)}\n`, { mode: 0o600 });
48
+ renameSync(temporary, file);
49
+ }