nixamp 0.39.2 → 0.40.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 (54) hide show
  1. package/dist/ai-radio.d.ts +2 -0
  2. package/dist/ai-radio.js +9 -0
  3. package/dist/ai-routes.d.ts +3 -0
  4. package/dist/ai-routes.js +26 -1
  5. package/dist/listener-shares.d.ts +2 -0
  6. package/dist/listener-shares.js +4 -0
  7. package/dist/server.d.ts +8 -1
  8. package/dist/server.js +78 -1
  9. package/dist/station-cli.js +31 -0
  10. package/dist/station-mcp.d.ts +144 -14
  11. package/dist/station-mcp.js +25 -0
  12. package/dist/station-routes.d.ts +3 -0
  13. package/dist/station-routes.js +22 -3
  14. package/dist/station-usage.d.ts +1 -1
  15. package/dist/station-usage.js +5 -0
  16. package/dist/support.d.ts +43 -0
  17. package/dist/support.js +203 -0
  18. package/dist/track-facts.d.ts +88 -0
  19. package/dist/track-facts.js +220 -0
  20. package/package.json +1 -1
  21. package/src/ai-radio.ts +9 -0
  22. package/src/ai-routes.ts +29 -1
  23. package/src/listener-shares.ts +5 -0
  24. package/src/server.ts +77 -1
  25. package/src/station-cli.ts +22 -0
  26. package/src/station-mcp.ts +24 -0
  27. package/src/station-routes.ts +23 -3
  28. package/src/station-usage.ts +5 -0
  29. package/src/support.ts +206 -0
  30. package/src/track-facts.ts +243 -0
  31. package/web/dist/assets/{ar-BSu384wE.js → ar-BY-kuED6.js} +1 -1
  32. package/web/dist/assets/{cs-CycweUsX.js → cs-C6mMbdyp.js} +1 -1
  33. package/web/dist/assets/{da-C7FbLd8g.js → da-DqV68WRi.js} +1 -1
  34. package/web/dist/assets/{de-DMcsxdff.js → de-C4JKBX1z.js} +1 -1
  35. package/web/dist/assets/{es-VnyHTJVp.js → es-B_mF5h1R.js} +1 -1
  36. package/web/dist/assets/{fi-B-oW_Ilc.js → fi-CDbSPL76.js} +1 -1
  37. package/web/dist/assets/{fr-CdEDQS5F.js → fr-B_573mv-.js} +1 -1
  38. package/web/dist/assets/{hi-B_xiLY1e.js → hi--YR7-iSh.js} +1 -1
  39. package/web/dist/assets/{hu-kFISDmOM.js → hu-kotmcF8p.js} +1 -1
  40. package/web/dist/assets/{id-DMel0ct7.js → id-CY1mU0Fh.js} +1 -1
  41. package/web/dist/assets/{it-1LsKNR45.js → it-Dj4xHmAf.js} +1 -1
  42. package/web/dist/assets/{main-CJLyq0BI.js → main-CwBTD5i3.js} +2 -2
  43. package/web/dist/assets/{nl-bZUFxlcO.js → nl-DVvshFko.js} +1 -1
  44. package/web/dist/assets/{ru-BI9zEd6Z.js → ru-BMwolR6_.js} +1 -1
  45. package/web/dist/assets/station-BZ2CtCcH.js +1 -0
  46. package/web/dist/assets/{station-Bq-LiY6G.css → station-CM31TuOq.css} +1 -1
  47. package/web/dist/assets/{sv-CD5WRIGc.js → sv-B_VngG1H.js} +1 -1
  48. package/web/dist/assets/{uk-BVzlKtfs.js → uk-D9pKHqQ7.js} +1 -1
  49. package/web/dist/assets/{vi-BV-dHawO.js → vi-BezqX85w.js} +1 -1
  50. package/web/dist/assets/{zh-DiT7lmM0.js → zh-CjTmbtGB.js} +1 -1
  51. package/web/dist/index.html +1 -1
  52. package/web/dist/station.html +7 -2
  53. package/web/dist/sw.js +21 -21
  54. package/web/dist/assets/station-DDvu2lUI.js +0 -1
@@ -125,6 +125,8 @@ export declare class AiRadio {
125
125
  private composer;
126
126
  list(): AiStation[];
127
127
  get(key: string): AiStation | undefined;
128
+ /** The track AI radio composed at this path, if it did. */
129
+ trackAt(file: string): AiTrack | undefined;
128
130
  /** What the owner sees: the stations, this month's seconds, and why nothing is being made. */
129
131
  status(): Record<string, unknown>;
130
132
  /** Change the month's cap; `resume` clears a stop for lack of credit. */
package/dist/ai-radio.js CHANGED
@@ -218,6 +218,15 @@ export class AiRadio {
218
218
  get(key) {
219
219
  return this.stations.get(key);
220
220
  }
221
+ /** The track AI radio composed at this path, if it did. */
222
+ trackAt(file) {
223
+ for (const station of this.stations.values()) {
224
+ const hit = station.tracks.find((one) => one.file === file);
225
+ if (hit)
226
+ return hit;
227
+ }
228
+ return undefined;
229
+ }
221
230
  /** What the owner sees: the stations, this month's seconds, and why nothing is being made. */
222
231
  status() {
223
232
  const month = monthOf(this.now());
@@ -11,11 +11,14 @@
11
11
  import type { IncomingMessage, ServerResponse } from "node:http";
12
12
  import type { AiRadio } from "./ai-radio.ts";
13
13
  import { type ListenerShares } from "./listener-shares.ts";
14
+ import type { HolderStore } from "./track-facts.ts";
14
15
  /** Whether this path is one anybody may use without the server's key. */
15
16
  export declare function isPublicSharePath(path: string): boolean;
16
17
  export interface AiRoutesContext {
17
18
  ai?: AiRadio;
18
19
  shares?: ListenerShares;
20
+ /** The copyright holders the owner registered, for the now-playing card. */
21
+ holders?: HolderStore;
19
22
  canControl: boolean;
20
23
  /** Who is asking, for per-sharer limits (an address; hashed before it is kept). */
21
24
  caller: string;
package/dist/ai-routes.js CHANGED
@@ -25,7 +25,8 @@ async function jsonBody(ctx, request) {
25
25
  export async function handleAiRoutes(request, response, path, url, ctx) {
26
26
  const method = request.method ?? "GET";
27
27
  const publicPath = PUBLIC.exec(path);
28
- const ownerPath = path === "/api/v1/stations/ai" || path.startsWith("/api/v1/stations/ai/") || path === "/api/v1/stations/shares" || path.startsWith("/api/v1/stations/shares/");
28
+ const ownerPath = path === "/api/v1/stations/ai" || path.startsWith("/api/v1/stations/ai/") || path === "/api/v1/stations/shares" || path.startsWith("/api/v1/stations/shares/")
29
+ || path === "/api/v1/stations/holders" || path.startsWith("/api/v1/stations/holders/");
29
30
  if (!publicPath && !ownerPath)
30
31
  return false;
31
32
  try {
@@ -81,6 +82,30 @@ export async function handleAiRoutes(request, response, path, url, ctx) {
81
82
  ctx.json(response, 403, { error: "only this server's owner manages AI radio and shares", code: "forbidden" });
82
83
  return true;
83
84
  }
85
+ if (path === "/api/v1/stations/holders" || path.startsWith("/api/v1/stations/holders/")) {
86
+ const holders = ctx.holders;
87
+ if (!holders) {
88
+ ctx.json(response, 404, { error: "copyright holders are not kept here", code: "not_found" });
89
+ return true;
90
+ }
91
+ if (path === "/api/v1/stations/holders" && method === "GET") {
92
+ ctx.json(response, 200, { holders: holders.list() });
93
+ return true;
94
+ }
95
+ if (path === "/api/v1/stations/holders" && method === "POST") {
96
+ ctx.json(response, 200, { holder: holders.set(await jsonBody(ctx, request)) });
97
+ return true;
98
+ }
99
+ const one = /^\/api\/v1\/stations\/holders\/([a-z0-9-]{1,40})$/.exec(path);
100
+ if (one && method === "DELETE") {
101
+ if (!holders.remove(one[1]))
102
+ throw new StationError("not_found", "no such holder", 404);
103
+ ctx.json(response, 200, { deleted: one[1] });
104
+ return true;
105
+ }
106
+ ctx.json(response, 404, { error: "no such holder route", code: "not_found" });
107
+ return true;
108
+ }
84
109
  if (path === "/api/v1/stations/shares" || path.startsWith("/api/v1/stations/shares/")) {
85
110
  const shares = ctx.shares;
86
111
  if (!shares) {
@@ -101,6 +101,8 @@ export declare class ListenerShares {
101
101
  configure(input: Record<string, unknown>): ShareSettings;
102
102
  list(state?: string): Share[];
103
103
  get(id: string): Share | undefined;
104
+ /** The share whose copy is this file, if it is one. */
105
+ atFile(file: string): Share | undefined;
104
106
  /** The share, if this token is the one its sharer was given. */
105
107
  forSharer(id: string, token: string): Share | undefined;
106
108
  private fromOf;
@@ -146,6 +146,10 @@ export class ListenerShares {
146
146
  get(id) {
147
147
  return this.shares.get(id);
148
148
  }
149
+ /** The share whose copy is this file, if it is one. */
150
+ atFile(file) {
151
+ return [...this.shares.values()].find((one) => one.file === file);
152
+ }
149
153
  /** The share, if this token is the one its sharer was given. */
150
154
  forSharer(id, token) {
151
155
  const share = this.shares.get(id);
package/dist/server.d.ts CHANGED
@@ -26,9 +26,10 @@ import { ListenerStore } from "./library-user.ts";
26
26
  import { Importer } from "./handoff.ts";
27
27
  import { AiRadio } from "./ai-radio.ts";
28
28
  import { ListenerShares } from "./listener-shares.ts";
29
+ import { Support } from "./support.ts";
29
30
  import { PeerGuides } from "./station-guide.ts";
30
31
  import { StationRunner } from "./station-runner.ts";
31
- import { StationStore } from "./stations.ts";
32
+ import { StationStore, type Station } from "./stations.ts";
32
33
  import { StationUsage, type Limits } from "./station-metering.ts";
33
34
  import { CompressionService } from "./compression/service.ts";
34
35
  import { Enricher } from "./enrich.ts";
@@ -598,6 +599,10 @@ export interface HandlerOptions {
598
599
  ai?: AiRadio;
599
600
  /** Tracks anybody may share, sorted onto the air by AI. */
600
601
  shares?: ListenerShares;
602
+ /** What a listener may know about the track on the air. */
603
+ facts?: (station: Station, file: string) => Promise<import("./track-facts.ts").TrackFacts>;
604
+ /** Copyright holders, matched to tracks by artist or station. */
605
+ holders?: import("./track-facts.ts").HolderStore;
601
606
  /** Proxies (by address) whose X-Forwarded-For is believed, for per-sharer limits. */
602
607
  trustedProxies?: () => Set<string>;
603
608
  /** Other servers' public stations, for this guide to list beside its own. */
@@ -795,6 +800,8 @@ export interface HandlerOptions {
795
800
  speech?: Speech;
796
801
  liveVoice?: LiveVoice;
797
802
  translationPasses?: TranslationPasses;
803
+ /** Payments to copyright holders nixamp.com collects for (support.ts). */
804
+ support?: Support;
798
805
  /** Signing secret of nixamp's CoinPay business, for /api/v1/webhooks/coinpay. */
799
806
  coinpayWebhookSecret?: string;
800
807
  sharedTranslations?: SharedTranslations;
package/dist/server.js CHANGED
@@ -63,6 +63,8 @@ import { handleHandoffs } from "./handoff-routes.js";
63
63
  import { handleAiRoutes, isPublicSharePath } from "./ai-routes.js";
64
64
  import { AiRadio } from "./ai-radio.js";
65
65
  import { ListenerShares } from "./listener-shares.js";
66
+ import { HolderStore, TagReader, factsFor } from "./track-facts.js";
67
+ import { SUPPORT_CSP, SUPPORT_PAGE, Support, SupportError, supportHoldersFrom } from "./support.js";
66
68
  import { STATION_APP_CSP, STATION_PAGE, STATION_PAGE_CSP, STATION_PAGE_HTML } from "./station-page.js";
67
69
  import { GUIDE_CSP, guideHosts, guidePeers, peerBase, PeerGuides, renderGuide } from "./station-guide.js";
68
70
  import { StationRunner } from "./station-runner.js";
@@ -2161,6 +2163,8 @@ export function createHandler(engine, options) {
2161
2163
  !isPublicSharePath(path) &&
2162
2164
  // What is on, as MCP, for anybody.
2163
2165
  path !== "/mcp/public" &&
2166
+ // Supporting a copyright holder is for anybody listening.
2167
+ !/^\/support\/[a-z0-9-]{1,40}$/.test(path) && !/^\/api\/v1\/support\/[a-z0-9-]{1,40}$/.test(path) &&
2164
2168
  // A station's short link, which sends the browser to wherever it plays.
2165
2169
  !/^\/watch\/[a-z0-9]{10}$/.test(path) &&
2166
2170
  !STATION_PAGE.test(path) &&
@@ -2227,6 +2231,57 @@ export function createHandler(engine, options) {
2227
2231
  * in the directory). It sends the browser to the station's own page, on
2228
2232
  * the server that plays it. Nothing private or unlisted is ever found.
2229
2233
  */
2234
+ /* Support a copyright holder: the page, the coins, and a CoinPay checkout. */
2235
+ const supportPage = /^\/support\/([a-z0-9-]{1,40})$/.exec(path);
2236
+ if (supportPage && options.support && (request.method === "GET" || request.method === "HEAD")) {
2237
+ if (!options.support.holder(supportPage[1])) {
2238
+ json(response, 404, { error: "nobody by that name is supported here", code: "not_found" });
2239
+ return;
2240
+ }
2241
+ response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache", "content-security-policy": SUPPORT_CSP, "referrer-policy": "no-referrer", "x-content-type-options": "nosniff" });
2242
+ response.end(request.method === "HEAD" ? undefined : SUPPORT_PAGE);
2243
+ return;
2244
+ }
2245
+ const supportApi = /^\/api\/v1\/support\/([a-z0-9-]{1,40})$/.exec(path);
2246
+ if (supportApi && options.support) {
2247
+ const support = options.support;
2248
+ const holder = support.holder(supportApi[1]);
2249
+ if (!holder) {
2250
+ json(response, 404, { error: "nobody by that name is supported here", code: "not_found" });
2251
+ return;
2252
+ }
2253
+ try {
2254
+ if (request.method === "GET") {
2255
+ const { coins } = support.enabled ? await support.coins() : { coins: [] };
2256
+ json(response, 200, { id: holder.id, name: holder.name, coins, ...(support.enabled ? {} : { error: "Support payments are not set up here." }) });
2257
+ return;
2258
+ }
2259
+ if (request.method === "POST") {
2260
+ const verdict = guard.check(`support:${callerOf(request.headers, request.socket.remoteAddress, behindProxy)}`, { allowed: 10, windowMs: 60_000 });
2261
+ if (!verdict.ok) {
2262
+ json(response, 429, { error: "too many at once; wait a minute", code: "rate_limited", retryAfter: verdict.retryAfter });
2263
+ return;
2264
+ }
2265
+ let body = {};
2266
+ try {
2267
+ body = JSON.parse(await readBody(request));
2268
+ }
2269
+ catch {
2270
+ body = {};
2271
+ }
2272
+ json(response, 200, await support.checkout({ ...body, holder: holder.id }));
2273
+ return;
2274
+ }
2275
+ json(response, 405, { error: "GET or POST", code: "method_not_allowed" });
2276
+ }
2277
+ catch (error) {
2278
+ if (error instanceof SupportError)
2279
+ json(response, error.status, { error: error.message, code: error.code });
2280
+ else
2281
+ json(response, 502, { error: "the payment service did not answer; nothing was charged", code: "provider" });
2282
+ }
2283
+ return;
2284
+ }
2230
2285
  const stationShortLink = /^\/watch\/([a-z0-9]{10})$/.exec(path);
2231
2286
  if (stationShortLink && options.stations && (request.method === "GET" || request.method === "HEAD")) {
2232
2287
  const encrypted = request.socket.encrypted === true;
@@ -2325,7 +2380,7 @@ export function createHandler(engine, options) {
2325
2380
  });
2326
2381
  return;
2327
2382
  }
2328
- if (options.stations && (isPublicSharePath(path) || path.startsWith("/api/v1/stations/ai") || path.startsWith("/api/v1/stations/shares"))) {
2383
+ if (options.stations && (isPublicSharePath(path) || path.startsWith("/api/v1/stations/ai") || path.startsWith("/api/v1/stations/shares") || path.startsWith("/api/v1/stations/holders"))) {
2329
2384
  const control = await controlOf();
2330
2385
  // Behind a proxy we trust (dev2 for live.streaming), the sharer is the
2331
2386
  // first address it names; otherwise whoever connected.
@@ -2335,6 +2390,7 @@ export function createHandler(engine, options) {
2335
2390
  const handled = await handleAiRoutes(request, response, path, url, {
2336
2391
  ...(options.stations.ai ? { ai: options.stations.ai } : {}),
2337
2392
  ...(options.stations.shares ? { shares: options.stations.shares } : {}),
2393
+ ...(options.stations.holders ? { holders: options.stations.holders } : {}),
2338
2394
  canControl: control.allowed,
2339
2395
  caller,
2340
2396
  json,
@@ -2387,6 +2443,7 @@ export function createHandler(engine, options) {
2387
2443
  ...(options.stations.usage ? { usage: options.stations.usage } : {}),
2388
2444
  ...(options.stations.limits ? { limits: options.stations.limits } : {}),
2389
2445
  ...(options.stations.auto ? { auto: options.stations.auto } : {}),
2446
+ ...(options.stations.facts ? { facts: options.stations.facts } : {}),
2390
2447
  ...(options.stations.peers ? { peers: () => options.stations.peers.entries() } : {}),
2391
2448
  throttle: () => {
2392
2449
  const verdict = guard.check(`stations:${control.account || callerOf(request.headers, request.socket.remoteAddress, behindProxy)}`, STATION_CONTROL_LIMIT);
@@ -6517,6 +6574,23 @@ export async function serve(argv, version = "0.1.0") {
6517
6574
  });
6518
6575
  stations.ai = aiRadio;
6519
6576
  stations.shares = listenerShares;
6577
+ // The now-playing card: the file's own tags, where it came from here, and
6578
+ // the copyright holders the owner registered.
6579
+ const tagReader = new TagReader(tools.ffprobe);
6580
+ const holderStore = new HolderStore(HolderStore.fileFor(stateDir(), options.port));
6581
+ stations.holders = holderStore;
6582
+ stations.facts = async (station, file) => {
6583
+ const share = listenerShares.atFile(file);
6584
+ const composed = aiRadio.trackAt(file);
6585
+ return factsFor({
6586
+ station, file,
6587
+ tags: await tagReader.read(file),
6588
+ known: trackFor(engine, file),
6589
+ holders: holderStore.list(),
6590
+ share: share ? { basis: share.rights.basis, note: share.rights.note, by: share.by } : null,
6591
+ aiTrack: composed ? { made: composed.made } : null,
6592
+ });
6593
+ };
6520
6594
  // Channels as HLS, on demand, for Safari on a phone: one ffmpeg copying a
6521
6595
  // channel's fragments into short files while somebody is asking for them.
6522
6596
  // What things are, from nichedb.dev, remembered beside the keys so a
@@ -6644,6 +6718,8 @@ export async function serve(argv, version = "0.1.0") {
6644
6718
  // is temporarily unconfigured. Self-hosters can explicitly sponsor usage.
6645
6719
  const translationPasses = accounts && pool && process.env["NIXAMP_TRANSLATION_BILLING"] !== "off"
6646
6720
  ? new TranslationPasses({ db: pool, key: coinpayKeyFrom(), site: nixampSite }) : undefined;
6721
+ // Payments to the copyright holders nixamp.com collects for, on its own CoinPay business.
6722
+ const supportPayments = accounts ? new Support({ key: coinpayKeyFrom(), site: nixampSite, holders: supportHoldersFrom() }) : undefined;
6647
6723
  if (translationPasses)
6648
6724
  void translationPasses.access().catch(() => console.error("nixamp: translation checkout could not warm; it will retry on demand"));
6649
6725
  const liveVoice = accounts && process.env["NIXAMP_DUBBING"] !== "off" ? new LiveVoice({
@@ -7192,6 +7268,7 @@ export async function serve(argv, version = "0.1.0") {
7192
7268
  ...(speech ? { speech } : {}),
7193
7269
  ...(liveVoice ? { liveVoice } : {}),
7194
7270
  ...(translationPasses ? { translationPasses } : {}),
7271
+ ...(supportPayments ? { support: supportPayments } : {}),
7195
7272
  ...(process.env["COINPAY_WEBHOOK_SECRET"] ? { coinpayWebhookSecret: process.env["COINPAY_WEBHOOK_SECRET"] } : {}),
7196
7273
  ...(sharedTranslations ? { sharedTranslations } : {}),
7197
7274
  ...(translator ? { translator } : {}),
@@ -265,6 +265,37 @@ export async function station(argv) {
265
265
  return await aiCommand(argv.slice(1), call, show, json);
266
266
  case "shares":
267
267
  return await sharesCommand(argv.slice(1), call, show);
268
+ case "holder":
269
+ case "holders": {
270
+ const sub = argv[1] ?? "list";
271
+ const all = (flagName) => argv.flatMap((one, at) => (one === flagName && argv[at + 1] ? [argv[at + 1]] : []));
272
+ const line = (h) => `${String(h["name"])} [${String(h["id"])}]\n artists ${(h["artists"] ?? []).join(", ") || "-"}\n stations ${(h["stations"] ?? []).join(", ") || "-"}\n support ${String(h["support"] || "-")}${h["license"] ? `\n licence ${String(h["license"])}` : ""}`;
273
+ if (sub === "list")
274
+ return show(await call("GET", "/api/v1/stations/holders"), (body) => (body["holders"] ?? []).map(line).join("\n\n") || "No copyright holders yet. nixamp station holder set NAME --artist ARTIST --support URL");
275
+ if (sub === "remove")
276
+ return show(await call("DELETE", `/api/v1/stations/holders/${argv[2] ?? ""}`), () => `Removed ${argv[2]}.`);
277
+ if (sub === "set") {
278
+ const name = argv[2];
279
+ if (!name || name.startsWith("--")) {
280
+ console.error("nixamp station holder set needs a NAME");
281
+ return 2;
282
+ }
283
+ const body = { name };
284
+ if (all("--artist").length)
285
+ body["artists"] = all("--artist");
286
+ if (all("--station").length)
287
+ body["stations"] = all("--station");
288
+ if (flag(argv, "--support"))
289
+ body["support"] = flag(argv, "--support");
290
+ if (flag(argv, "--label"))
291
+ body["label"] = flag(argv, "--label");
292
+ if (flag(argv, "--license"))
293
+ body["license"] = flag(argv, "--license");
294
+ return show(await call("POST", "/api/v1/stations/holders", body), (answer) => line(answer["holder"]));
295
+ }
296
+ console.error(`nixamp station holder: no command "${sub}".\n\n${USAGE}`);
297
+ return 2;
298
+ }
268
299
  case "delete": {
269
300
  const station = needId();
270
301
  if (!station)
@@ -28,7 +28,6 @@ export declare const STATION_TOOLS: ({
28
28
  off?: undefined;
29
29
  key?: undefined;
30
30
  preset?: undefined;
31
- name?: undefined;
32
31
  style?: undefined;
33
32
  description?: undefined;
34
33
  vocals?: undefined;
@@ -36,7 +35,6 @@ export declare const STATION_TOOLS: ({
36
35
  perDay?: undefined;
37
36
  enabled?: undefined;
38
37
  monthlySeconds?: undefined;
39
- action?: undefined;
40
38
  state?: undefined;
41
39
  reason?: undefined;
42
40
  open?: undefined;
@@ -44,6 +42,13 @@ export declare const STATION_TOOLS: ({
44
42
  publish?: undefined;
45
43
  maxMb?: undefined;
46
44
  perHour?: undefined;
45
+ action?: undefined;
46
+ name?: undefined;
47
+ artists?: undefined;
48
+ stations?: undefined;
49
+ support?: undefined;
50
+ label?: undefined;
51
+ license?: undefined;
47
52
  id?: undefined;
48
53
  private?: undefined;
49
54
  };
@@ -83,7 +88,6 @@ export declare const STATION_TOOLS: ({
83
88
  off?: undefined;
84
89
  key?: undefined;
85
90
  preset?: undefined;
86
- name?: undefined;
87
91
  style?: undefined;
88
92
  description?: undefined;
89
93
  vocals?: undefined;
@@ -91,7 +95,6 @@ export declare const STATION_TOOLS: ({
91
95
  perDay?: undefined;
92
96
  enabled?: undefined;
93
97
  monthlySeconds?: undefined;
94
- action?: undefined;
95
98
  state?: undefined;
96
99
  reason?: undefined;
97
100
  open?: undefined;
@@ -99,6 +102,13 @@ export declare const STATION_TOOLS: ({
99
102
  publish?: undefined;
100
103
  maxMb?: undefined;
101
104
  perHour?: undefined;
105
+ action?: undefined;
106
+ name?: undefined;
107
+ artists?: undefined;
108
+ stations?: undefined;
109
+ support?: undefined;
110
+ label?: undefined;
111
+ license?: undefined;
102
112
  private?: undefined;
103
113
  };
104
114
  required: string[];
@@ -168,7 +178,6 @@ export declare const STATION_TOOLS: ({
168
178
  perDay?: undefined;
169
179
  enabled?: undefined;
170
180
  monthlySeconds?: undefined;
171
- action?: undefined;
172
181
  state?: undefined;
173
182
  reason?: undefined;
174
183
  open?: undefined;
@@ -176,6 +185,12 @@ export declare const STATION_TOOLS: ({
176
185
  publish?: undefined;
177
186
  maxMb?: undefined;
178
187
  perHour?: undefined;
188
+ action?: undefined;
189
+ artists?: undefined;
190
+ stations?: undefined;
191
+ support?: undefined;
192
+ label?: undefined;
193
+ license?: undefined;
179
194
  id?: undefined;
180
195
  private?: undefined;
181
196
  };
@@ -243,7 +258,6 @@ export declare const STATION_TOOLS: ({
243
258
  perDay?: undefined;
244
259
  enabled?: undefined;
245
260
  monthlySeconds?: undefined;
246
- action?: undefined;
247
261
  state?: undefined;
248
262
  reason?: undefined;
249
263
  open?: undefined;
@@ -251,6 +265,12 @@ export declare const STATION_TOOLS: ({
251
265
  publish?: undefined;
252
266
  maxMb?: undefined;
253
267
  perHour?: undefined;
268
+ action?: undefined;
269
+ artists?: undefined;
270
+ stations?: undefined;
271
+ support?: undefined;
272
+ label?: undefined;
273
+ license?: undefined;
254
274
  private?: undefined;
255
275
  };
256
276
  required: string[];
@@ -293,7 +313,6 @@ export declare const STATION_TOOLS: ({
293
313
  off?: undefined;
294
314
  key?: undefined;
295
315
  preset?: undefined;
296
- name?: undefined;
297
316
  style?: undefined;
298
317
  description?: undefined;
299
318
  vocals?: undefined;
@@ -308,6 +327,12 @@ export declare const STATION_TOOLS: ({
308
327
  publish?: undefined;
309
328
  maxMb?: undefined;
310
329
  perHour?: undefined;
330
+ name?: undefined;
331
+ artists?: undefined;
332
+ stations?: undefined;
333
+ support?: undefined;
334
+ label?: undefined;
335
+ license?: undefined;
311
336
  private?: undefined;
312
337
  };
313
338
  required: string[];
@@ -357,7 +382,6 @@ export declare const STATION_TOOLS: ({
357
382
  off?: undefined;
358
383
  key?: undefined;
359
384
  preset?: undefined;
360
- name?: undefined;
361
385
  style?: undefined;
362
386
  description?: undefined;
363
387
  vocals?: undefined;
@@ -365,7 +389,6 @@ export declare const STATION_TOOLS: ({
365
389
  perDay?: undefined;
366
390
  enabled?: undefined;
367
391
  monthlySeconds?: undefined;
368
- action?: undefined;
369
392
  state?: undefined;
370
393
  reason?: undefined;
371
394
  open?: undefined;
@@ -373,6 +396,13 @@ export declare const STATION_TOOLS: ({
373
396
  publish?: undefined;
374
397
  maxMb?: undefined;
375
398
  perHour?: undefined;
399
+ action?: undefined;
400
+ name?: undefined;
401
+ artists?: undefined;
402
+ stations?: undefined;
403
+ support?: undefined;
404
+ label?: undefined;
405
+ license?: undefined;
376
406
  private?: undefined;
377
407
  };
378
408
  required: string[];
@@ -421,7 +451,6 @@ export declare const STATION_TOOLS: ({
421
451
  };
422
452
  key?: undefined;
423
453
  preset?: undefined;
424
- name?: undefined;
425
454
  style?: undefined;
426
455
  description?: undefined;
427
456
  vocals?: undefined;
@@ -429,7 +458,6 @@ export declare const STATION_TOOLS: ({
429
458
  perDay?: undefined;
430
459
  enabled?: undefined;
431
460
  monthlySeconds?: undefined;
432
- action?: undefined;
433
461
  state?: undefined;
434
462
  reason?: undefined;
435
463
  open?: undefined;
@@ -437,6 +465,13 @@ export declare const STATION_TOOLS: ({
437
465
  publish?: undefined;
438
466
  maxMb?: undefined;
439
467
  perHour?: undefined;
468
+ action?: undefined;
469
+ name?: undefined;
470
+ artists?: undefined;
471
+ stations?: undefined;
472
+ support?: undefined;
473
+ label?: undefined;
474
+ license?: undefined;
440
475
  id?: undefined;
441
476
  private?: undefined;
442
477
  };
@@ -515,6 +550,11 @@ export declare const STATION_TOOLS: ({
515
550
  moderation?: undefined;
516
551
  maxMb?: undefined;
517
552
  perHour?: undefined;
553
+ artists?: undefined;
554
+ stations?: undefined;
555
+ support?: undefined;
556
+ label?: undefined;
557
+ license?: undefined;
518
558
  id?: undefined;
519
559
  private?: undefined;
520
560
  };
@@ -546,7 +586,6 @@ export declare const STATION_TOOLS: ({
546
586
  off?: undefined;
547
587
  key?: undefined;
548
588
  preset?: undefined;
549
- name?: undefined;
550
589
  style?: undefined;
551
590
  description?: undefined;
552
591
  vocals?: undefined;
@@ -589,6 +628,12 @@ export declare const STATION_TOOLS: ({
589
628
  perHour: {
590
629
  type: string;
591
630
  };
631
+ name?: undefined;
632
+ artists?: undefined;
633
+ stations?: undefined;
634
+ support?: undefined;
635
+ label?: undefined;
636
+ license?: undefined;
592
637
  private?: undefined;
593
638
  };
594
639
  required?: undefined;
@@ -619,7 +664,6 @@ export declare const STATION_TOOLS: ({
619
664
  off?: undefined;
620
665
  key?: undefined;
621
666
  preset?: undefined;
622
- name?: undefined;
623
667
  style?: undefined;
624
668
  description?: undefined;
625
669
  vocals?: undefined;
@@ -627,7 +671,6 @@ export declare const STATION_TOOLS: ({
627
671
  perDay?: undefined;
628
672
  enabled?: undefined;
629
673
  monthlySeconds?: undefined;
630
- action?: undefined;
631
674
  state?: undefined;
632
675
  reason?: undefined;
633
676
  open?: undefined;
@@ -639,6 +682,93 @@ export declare const STATION_TOOLS: ({
639
682
  type: "string";
640
683
  description: string;
641
684
  };
685
+ action: {
686
+ type: string;
687
+ enum: string[];
688
+ };
689
+ id: {
690
+ readonly type: "string";
691
+ };
692
+ name: {
693
+ readonly type: "string";
694
+ };
695
+ artists: {
696
+ type: string;
697
+ items: {
698
+ readonly type: "string";
699
+ };
700
+ };
701
+ stations: {
702
+ type: string;
703
+ items: {
704
+ readonly type: "string";
705
+ };
706
+ };
707
+ support: {
708
+ type: "string";
709
+ description: string;
710
+ };
711
+ label: {
712
+ readonly type: "string";
713
+ };
714
+ license: {
715
+ readonly type: "string";
716
+ };
717
+ private?: undefined;
718
+ };
719
+ required?: undefined;
720
+ };
721
+ } | {
722
+ name: string;
723
+ description: string;
724
+ inputSchema: {
725
+ type: string;
726
+ properties: {
727
+ playlist?: undefined;
728
+ idempotencyKey?: undefined;
729
+ folder?: undefined;
730
+ items?: undefined;
731
+ shuffle?: undefined;
732
+ repeat?: undefined;
733
+ bitrate?: undefined;
734
+ visibility?: undefined;
735
+ listed?: undefined;
736
+ basis?: undefined;
737
+ note?: undefined;
738
+ video?: undefined;
739
+ minItems?: undefined;
740
+ minFilms?: undefined;
741
+ maxVideoRunning?: undefined;
742
+ breaksEvery?: undefined;
743
+ start?: undefined;
744
+ off?: undefined;
745
+ key?: undefined;
746
+ preset?: undefined;
747
+ style?: undefined;
748
+ description?: undefined;
749
+ vocals?: undefined;
750
+ trackSeconds?: undefined;
751
+ perDay?: undefined;
752
+ enabled?: undefined;
753
+ monthlySeconds?: undefined;
754
+ state?: undefined;
755
+ reason?: undefined;
756
+ open?: undefined;
757
+ moderation?: undefined;
758
+ publish?: undefined;
759
+ maxMb?: undefined;
760
+ perHour?: undefined;
761
+ action?: undefined;
762
+ name?: undefined;
763
+ artists?: undefined;
764
+ stations?: undefined;
765
+ support?: undefined;
766
+ label?: undefined;
767
+ license?: undefined;
768
+ server: {
769
+ type: "string";
770
+ description: string;
771
+ };
642
772
  id: {
643
773
  type: "string";
644
774
  description: string;
@@ -121,6 +121,21 @@ export const STATION_TOOLS = [
121
121
  },
122
122
  },
123
123
  },
124
+ {
125
+ name: "station_holders",
126
+ description: "Copyright holders for the now-playing card: who holds a track's copyright (matched by artist name or station) and the link that supports them. action list (default), set (name, artists, stations, support https URL, label, license), remove (id).",
127
+ inputSchema: {
128
+ type: "object",
129
+ properties: {
130
+ server: SERVER,
131
+ action: { type: "string", enum: ["list", "set", "remove"] },
132
+ id: STRING, name: STRING,
133
+ artists: { type: "array", items: STRING }, stations: { type: "array", items: STRING },
134
+ support: { ...STRING, description: "https link where listeners subscribe or donate, e.g. https://nixamp.com/support/profullstack" },
135
+ label: STRING, license: STRING,
136
+ },
137
+ },
138
+ },
124
139
  {
125
140
  name: "station_unpublish",
126
141
  description: "Take a station out of the directory, or make it private: its links stop working and listeners are disconnected.",
@@ -200,6 +215,16 @@ export async function callStationTool(name, args, options = {}) {
200
215
  return say("action is status, create, generate, update, remove, budget or resume", true);
201
216
  return answer.ok ? say(JSON.stringify(answer.body, null, 2)) : refused(answer.body);
202
217
  }
218
+ if (name === "station_holders") {
219
+ const action = String(args["action"] ?? "list");
220
+ const answer = action === "list" ? await call("GET", "/api/v1/stations/holders")
221
+ : action === "set" ? await call("POST", "/api/v1/stations/holders", pick(["id", "name", "artists", "stations", "support", "label", "license"]))
222
+ : action === "remove" && typeof args["id"] === "string" ? await call("DELETE", `/api/v1/stations/holders/${encodeURIComponent(args["id"])}`)
223
+ : null;
224
+ if (!answer)
225
+ return say("action is list, set or remove (remove needs id)", true);
226
+ return answer.ok ? say(JSON.stringify(answer.body, null, 2)) : refused(answer.body);
227
+ }
203
228
  if (name === "station_shares") {
204
229
  const action = String(args["action"] ?? "status");
205
230
  const share = typeof args["id"] === "string" && /^[a-f0-9]{12}$/.test(args["id"]) ? args["id"] : "";