nixamp 0.51.0 → 0.52.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 (46) hide show
  1. package/dist/ai-label.d.ts +70 -3
  2. package/dist/ai-label.js +375 -29
  3. package/dist/label-routes.js +2 -1
  4. package/dist/main.js +9 -0
  5. package/dist/mcp.js +42 -0
  6. package/dist/music-cli.d.ts +2 -0
  7. package/dist/music-cli.js +109 -0
  8. package/dist/music-client.d.ts +62 -0
  9. package/dist/music-client.js +73 -0
  10. package/dist/station-cli.js +8 -1
  11. package/dist/station-mcp.d.ts +62 -0
  12. package/dist/station-mcp.js +5 -2
  13. package/dist/station-usage.d.ts +1 -1
  14. package/dist/station-usage.js +3 -0
  15. package/package.json +1 -1
  16. package/src/ai-label.ts +380 -29
  17. package/src/label-routes.ts +2 -1
  18. package/src/main.ts +8 -0
  19. package/src/mcp.ts +38 -0
  20. package/src/music-cli.ts +86 -0
  21. package/src/music-client.ts +86 -0
  22. package/src/station-cli.ts +8 -1
  23. package/src/station-mcp.ts +5 -2
  24. package/src/station-usage.ts +3 -0
  25. package/web/dist/assets/{ar--GRGOL7F.js → ar-DMx2aeAO.js} +1 -1
  26. package/web/dist/assets/{cs-B2ab-8qO.js → cs-Ddu0q3kG.js} +1 -1
  27. package/web/dist/assets/{da-Ek2g1uTg.js → da-CZ9gg_pb.js} +1 -1
  28. package/web/dist/assets/{de-CM7-9Fbk.js → de-CgJX4xg2.js} +1 -1
  29. package/web/dist/assets/{es-BFSOrr3I.js → es-Cp5ryDrJ.js} +1 -1
  30. package/web/dist/assets/{fi-D9Tf69sv.js → fi-D912EKux.js} +1 -1
  31. package/web/dist/assets/{fr-Gh9yYJGo.js → fr-FE_K0MPb.js} +1 -1
  32. package/web/dist/assets/{hi-B3ow3HJC.js → hi-BdYtQs1S.js} +1 -1
  33. package/web/dist/assets/{hu-paju-6Kr.js → hu-oh5tEg0p.js} +1 -1
  34. package/web/dist/assets/{id-mubOQFX0.js → id-NbCUWeB1.js} +1 -1
  35. package/web/dist/assets/{it-0EM-qnDT.js → it-CiHbKZ6Y.js} +1 -1
  36. package/web/dist/assets/{main-CECXge6G.js → main-8wV87D-g.js} +2 -2
  37. package/web/dist/assets/{nl-c6HalphH.js → nl-nLHK_AQ-.js} +1 -1
  38. package/web/dist/assets/{radio-CDUEoM0y.js → radio-BmHvBkVy.js} +1 -1
  39. package/web/dist/assets/{ru-CUKYVlgf.js → ru-BDdAS0Vv.js} +1 -1
  40. package/web/dist/assets/{sv-BUxBsZR5.js → sv-COweXM56.js} +1 -1
  41. package/web/dist/assets/{uk-708lBssa.js → uk-BJFzR1LS.js} +1 -1
  42. package/web/dist/assets/{vi-B1ropUvk.js → vi-qg2DJ6cR.js} +1 -1
  43. package/web/dist/assets/{zh-C_ndTsNh.js → zh-DUWOKDkb.js} +1 -1
  44. package/web/dist/index.html +2 -1
  45. package/web/dist/radio.html +1 -1
  46. package/web/dist/sw.js +20 -20
@@ -0,0 +1,2 @@
1
+ export declare const MUSIC_USAGE = "nixamp music: the AI label's songs as files (a song 10\u00A2, an album $1, everything by an artist $2; live radio is free)\n\n nixamp music [list] what you bought, with its download link\n nixamp music catalog [SERVER] the artists, albums and songs (live.streaming by default)\n nixamp music buy LINK a nixamp.com/buy link, or:\n nixamp music buy --server URL --kind song|album|artist --artist KEY [--album KEY] [--n N]\n [--email you@example.com] [--coin SOL] prints the CoinPay page to pay on\n nixamp music download ID|all [--out DIR] save the zip(s): MP3s with art and lyrics\n";
2
+ export declare function music(argv: string[]): Promise<number>;
@@ -0,0 +1,109 @@
1
+ /**
2
+ * `nixamp music`: the AI label's catalogue, buying, and what you bought.
3
+ *
4
+ * nixamp music [list] what you bought (signed in with that email)
5
+ * nixamp music catalog [SERVER] artists, albums and songs (live.streaming by default)
6
+ * nixamp music buy LINK|--server S --kind song|album|artist --artist A [--album B] [--n N]
7
+ * [--email E] [--coin SOL] prints the CoinPay page to pay on
8
+ * nixamp music download ID|all [--out DIR] the zip(s) of what you bought
9
+ *
10
+ * Listening is free on the live stations; the files are sold: a song 10¢, an
11
+ * album $1, everything by an artist $2.
12
+ */
13
+ import { buy, catalogue, download, fromBuyLink, PRICE_WORDS, purchases } from "./music-client.js";
14
+ export const MUSIC_USAGE = `nixamp music: the AI label's songs as files (${PRICE_WORDS}; live radio is free)
15
+
16
+ nixamp music [list] what you bought, with its download link
17
+ nixamp music catalog [SERVER] the artists, albums and songs (live.streaming by default)
18
+ nixamp music buy LINK a nixamp.com/buy link, or:
19
+ nixamp music buy --server URL --kind song|album|artist --artist KEY [--album KEY] [--n N]
20
+ [--email you@example.com] [--coin SOL] prints the CoinPay page to pay on
21
+ nixamp music download ID|all [--out DIR] save the zip(s): MP3s with art and lyrics
22
+ `;
23
+ function flag(argv, name) {
24
+ const at = argv.indexOf(name);
25
+ return at === -1 ? undefined : argv[at + 1];
26
+ }
27
+ export async function music(argv) {
28
+ const [sub = "list", arg] = argv;
29
+ const json = argv.includes("--json");
30
+ try {
31
+ if (sub === "help" || argv.includes("--help")) {
32
+ console.log(MUSIC_USAGE);
33
+ return 0;
34
+ }
35
+ if (sub === "list") {
36
+ const mine = await purchases();
37
+ if (json) {
38
+ console.log(JSON.stringify(mine, null, 2));
39
+ return 0;
40
+ }
41
+ if (mine.purchases.length === 0) {
42
+ console.log(`Nothing bought yet for ${mine.email}. Every AI label song is for sale: ${PRICE_WORDS}.`);
43
+ return 0;
44
+ }
45
+ for (const one of mine.purchases)
46
+ console.log(`${one.id} ${one.kind.padEnd(6)} ${one.title} · ${one.artistName} ${new Date(one.paidAt).toISOString().slice(0, 10)}\n ${one.zip ?? "(not paid yet)"}`);
47
+ return 0;
48
+ }
49
+ if (sub === "catalog" || sub === "catalogue") {
50
+ const view = await catalogue(arg && !arg.startsWith("--") ? arg : undefined);
51
+ if (json) {
52
+ console.log(JSON.stringify(view, null, 2));
53
+ return 0;
54
+ }
55
+ for (const genre of view.genres) {
56
+ console.log(`\n${genre.genre}${genre.station ? ` (live: /watch/${genre.station})` : ""}`);
57
+ for (const artist of genre.artists) {
58
+ console.log(` ${artist.name} [${artist.key}]`);
59
+ for (const album of artist.albums)
60
+ console.log(` ${album.year} ${album.title} [${album.key}] ${album.songs.length} songs${album.complete ? "" : " (recording)"}`);
61
+ }
62
+ }
63
+ console.log(`\n℗ ${view.holder}. ${PRICE_WORDS}: nixamp music buy --server ${view.server ?? "URL"} --kind album --artist KEY --album KEY`);
64
+ return 0;
65
+ }
66
+ if (sub === "buy") {
67
+ const fromLink = arg && !arg.startsWith("--") ? fromBuyLink(arg) : null;
68
+ if (arg && !arg.startsWith("--") && !fromLink) {
69
+ console.error("nixamp music buy: that is not a nixamp.com/buy link");
70
+ return 2;
71
+ }
72
+ const input = {
73
+ ...(fromLink ?? { server: flag(argv, "--server"), kind: flag(argv, "--kind"), artist: flag(argv, "--artist"), album: flag(argv, "--album"), n: flag(argv, "--n") ? Number(flag(argv, "--n")) : undefined }),
74
+ coin: flag(argv, "--coin") ?? "USDC_POL",
75
+ ...(flag(argv, "--email") ? { email: flag(argv, "--email") } : {}),
76
+ };
77
+ if (typeof input["n"] === "string")
78
+ input["n"] = Number(input["n"]);
79
+ const made = await buy(input);
80
+ if (json) {
81
+ console.log(JSON.stringify(made, null, 2));
82
+ return 0;
83
+ }
84
+ console.log(`Pay here: ${made.url}\nYour receipt and download, once paid: ${made.receipt}`);
85
+ return 0;
86
+ }
87
+ if (sub === "download") {
88
+ if (!arg) {
89
+ console.error("nixamp music download needs a purchase id (nixamp music) or all");
90
+ return 2;
91
+ }
92
+ const mine = await purchases();
93
+ const chosen = arg === "all" ? mine.purchases : mine.purchases.filter((one) => one.id === arg);
94
+ if (chosen.length === 0) {
95
+ console.error(`nixamp music: no purchase ${arg}`);
96
+ return 1;
97
+ }
98
+ for (const one of chosen)
99
+ console.log(`saved ${await download(one, flag(argv, "--out") ?? ".")}`);
100
+ return 0;
101
+ }
102
+ console.error(`nixamp music: no command "${sub}".\n\n${MUSIC_USAGE}`);
103
+ return 2;
104
+ }
105
+ catch (error) {
106
+ console.error(`nixamp music: ${error.message}`);
107
+ return 1;
108
+ }
109
+ }
@@ -0,0 +1,62 @@
1
+ export declare const DEFAULT_CATALOGUE = "https://live.streaming.pit.world";
2
+ export interface CatalogueSong {
3
+ n: number;
4
+ title: string;
5
+ seconds: number;
6
+ lyrics?: string;
7
+ }
8
+ export interface CatalogueAlbum {
9
+ key: string;
10
+ title: string;
11
+ year: number;
12
+ concept: string;
13
+ cover: boolean;
14
+ complete: boolean;
15
+ songs: CatalogueSong[];
16
+ }
17
+ export interface CatalogueArtist {
18
+ key: string;
19
+ name: string;
20
+ genre: string;
21
+ bio: string;
22
+ albums: CatalogueAlbum[];
23
+ }
24
+ export interface Catalogue {
25
+ holder: string;
26
+ server?: string;
27
+ genres: Array<{
28
+ genre: string;
29
+ station: string;
30
+ artists: CatalogueArtist[];
31
+ }>;
32
+ }
33
+ export interface Bought {
34
+ id: string;
35
+ kind: "song" | "album" | "artist";
36
+ title: string;
37
+ artistName: string;
38
+ server: string;
39
+ status: string;
40
+ paidAt: number;
41
+ amountUsd: number;
42
+ zip?: string;
43
+ link?: string;
44
+ }
45
+ type Fetcher = typeof fetch;
46
+ export declare function catalogue(server?: string, fetcher?: Fetcher): Promise<Catalogue>;
47
+ /** A /buy link read back into what it sells. */
48
+ export declare function fromBuyLink(link: string): Record<string, string> | null;
49
+ export declare function buy(input: Record<string, unknown>, fetcher?: Fetcher): Promise<{
50
+ url: string;
51
+ id: string;
52
+ token: string;
53
+ receipt: string;
54
+ }>;
55
+ export declare function purchases(fetcher?: Fetcher): Promise<{
56
+ email: string;
57
+ purchases: Bought[];
58
+ }>;
59
+ /** A purchase's zip, saved under `dir` with the name its server gives it. */
60
+ export declare function download(one: Bought, dir: string, fetcher?: Fetcher): Promise<string>;
61
+ export declare const PRICE_WORDS = "a song 10\u00A2, an album $1, everything by an artist $2";
62
+ export {};
@@ -0,0 +1,73 @@
1
+ /**
2
+ * The AI label's catalogue and the music store, for the terminal and MCP.
3
+ *
4
+ * The catalogue is read from the server that holds the music (live.streaming
5
+ * by default); buying and what was bought are nixamp.com's (music-store.ts),
6
+ * with this machine's sign-in when there is one.
7
+ */
8
+ import { createWriteStream, mkdirSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { Readable } from "node:stream";
11
+ import { pipeline } from "node:stream/promises";
12
+ import { readSession } from "./session.js";
13
+ export const DEFAULT_CATALOGUE = "https://live.streaming.pit.world";
14
+ function site() {
15
+ return (readSession()?.site ?? process.env["NIXAMP_SITE"] ?? "https://nixamp.com").replace(/\/+$/, "");
16
+ }
17
+ function auth() {
18
+ const token = readSession()?.token;
19
+ return token ? { authorization: `Bearer ${token}` } : {};
20
+ }
21
+ export async function catalogue(server = DEFAULT_CATALOGUE, fetcher = fetch) {
22
+ const response = await fetcher(`${server.replace(/\/+$/, "")}/api/v1/label`, { signal: AbortSignal.timeout(15_000) });
23
+ if (!response.ok)
24
+ throw new Error(`${server} has no AI label catalogue (HTTP ${response.status})`);
25
+ return await response.json();
26
+ }
27
+ /** A /buy link read back into what it sells. */
28
+ export function fromBuyLink(link) {
29
+ try {
30
+ const url = new URL(link);
31
+ if (url.pathname !== "/buy")
32
+ return null;
33
+ return Object.fromEntries(url.searchParams.entries());
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ export async function buy(input, fetcher = fetch) {
40
+ const response = await fetcher(`${site()}/api/v1/store/checkout`, {
41
+ method: "POST",
42
+ headers: { "content-type": "application/json", ...auth() },
43
+ body: JSON.stringify(input),
44
+ });
45
+ const body = await response.json().catch(() => ({}));
46
+ if (!response.ok || !body.url)
47
+ throw new Error(body.error ?? `nixamp.com answered ${response.status}`);
48
+ return { url: body.url, id: body.id ?? "", token: body.token ?? "", receipt: `${site()}/purchases/${body.id}?t=${body.token}` };
49
+ }
50
+ export async function purchases(fetcher = fetch) {
51
+ if (!readSession())
52
+ throw new Error("not signed in: run `nixamp login` (with the email you bought with)");
53
+ const response = await fetcher(`${site()}/api/v1/store/purchases`, { headers: auth() });
54
+ const body = await response.json().catch(() => ({}));
55
+ if (!response.ok)
56
+ throw new Error(body.error ?? `nixamp.com answered ${response.status}`);
57
+ return { email: body.email ?? "", purchases: body.purchases ?? [] };
58
+ }
59
+ /** A purchase's zip, saved under `dir` with the name its server gives it. */
60
+ export async function download(one, dir, fetcher = fetch) {
61
+ if (!one.zip)
62
+ throw new Error(`"${one.title}" is not paid yet`);
63
+ const response = await fetcher(one.zip);
64
+ if (!response.ok || !response.body)
65
+ throw new Error(`the download of "${one.title}" answered HTTP ${response.status}`);
66
+ const named = /filename\*=UTF-8''([^;]+)/.exec(response.headers.get("content-disposition") ?? "")?.[1];
67
+ const name = (named ? decodeURIComponent(named) : `${one.artistName} - ${one.title}.zip`).replace(/[/\\]/g, "");
68
+ mkdirSync(dir, { recursive: true });
69
+ const path = join(dir, name);
70
+ await pipeline(Readable.fromWeb(response.body), createWriteStream(path));
71
+ return path;
72
+ }
73
+ export const PRICE_WORDS = "a song 10¢, an album $1, everything by an artist $2";
@@ -466,7 +466,9 @@ async function labelCommand(argv, call, show) {
466
466
  `files ${String(body["root"])}/<genre>/<artist>/<year>-<album>/`,
467
467
  `composer ${String(body["composer"] ?? "none")}; words by ${String(body["writer"] ?? "nobody (no model key)")}`,
468
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`,
469
+ `albums ${String(settings["songsPerAlbum"])} songs of ${String(settings["songSeconds"])} s; ${String(settings["seedAlbums"])} to start, then one every ${String(settings["everyDays"])} days; ${String(settings["parallel"] ?? 1)} at a time`,
470
+ `genres ${(settings["genreOrder"] ?? []).join(", ")} first`,
471
+ `after that ${Number(settings["albumsPerDay"] ?? 0) > 0 ? `${String(settings["albumsPerDay"])} album a day, a new artist ${Math.round(Number(settings["newArtistChance"] ?? 0) * 100)}% of the time` : `one per artist every ${String(settings["everyDays"])} days`}`,
470
472
  ...(body["waiting"] ? [`waiting ${String(body["waiting"])}`] : []),
471
473
  "",
472
474
  ...(artists.length ? artists.map(describe) : ["Nobody signed yet. nixamp station label sign --roster"]),
@@ -530,6 +532,11 @@ async function labelCommand(argv, call, show) {
530
532
  seedAlbums: number("--seed-albums"),
531
533
  songsPerAlbum: number("--songs"),
532
534
  songSeconds: number("--seconds"),
535
+ parallel: number("--parallel"),
536
+ albumsPerDay: number("--per-day"),
537
+ videos: has(argv, "--no-videos") ? false : has(argv, "--videos") ? true : undefined,
538
+ newArtistChance: number("--new-artists"),
539
+ genreOrder: flag(argv, "--genres"),
533
540
  enabled: has(argv, "--off") ? false : has(argv, "--on") ? true : undefined,
534
541
  publish: has(argv, "--unlisted") ? false : has(argv, "--public") ? true : undefined,
535
542
  }).filter(([, value]) => value !== undefined));
@@ -46,6 +46,10 @@ export declare const STATION_TOOLS: ({
46
46
  seedAlbums?: undefined;
47
47
  songsPerAlbum?: undefined;
48
48
  songSeconds?: undefined;
49
+ parallel?: undefined;
50
+ albumsPerDay?: undefined;
51
+ newArtistChance?: undefined;
52
+ genreOrder?: undefined;
49
53
  state?: undefined;
50
54
  reason?: undefined;
51
55
  open?: undefined;
@@ -117,6 +121,10 @@ export declare const STATION_TOOLS: ({
117
121
  seedAlbums?: undefined;
118
122
  songsPerAlbum?: undefined;
119
123
  songSeconds?: undefined;
124
+ parallel?: undefined;
125
+ albumsPerDay?: undefined;
126
+ newArtistChance?: undefined;
127
+ genreOrder?: undefined;
120
128
  state?: undefined;
121
129
  reason?: undefined;
122
130
  open?: undefined;
@@ -211,6 +219,10 @@ export declare const STATION_TOOLS: ({
211
219
  seedAlbums?: undefined;
212
220
  songsPerAlbum?: undefined;
213
221
  songSeconds?: undefined;
222
+ parallel?: undefined;
223
+ albumsPerDay?: undefined;
224
+ newArtistChance?: undefined;
225
+ genreOrder?: undefined;
214
226
  state?: undefined;
215
227
  reason?: undefined;
216
228
  open?: undefined;
@@ -302,6 +314,10 @@ export declare const STATION_TOOLS: ({
302
314
  seedAlbums?: undefined;
303
315
  songsPerAlbum?: undefined;
304
316
  songSeconds?: undefined;
317
+ parallel?: undefined;
318
+ albumsPerDay?: undefined;
319
+ newArtistChance?: undefined;
320
+ genreOrder?: undefined;
305
321
  state?: undefined;
306
322
  reason?: undefined;
307
323
  open?: undefined;
@@ -375,6 +391,10 @@ export declare const STATION_TOOLS: ({
375
391
  seedAlbums?: undefined;
376
392
  songsPerAlbum?: undefined;
377
393
  songSeconds?: undefined;
394
+ parallel?: undefined;
395
+ albumsPerDay?: undefined;
396
+ newArtistChance?: undefined;
397
+ genreOrder?: undefined;
378
398
  state?: undefined;
379
399
  reason?: undefined;
380
400
  open?: undefined;
@@ -455,6 +475,10 @@ export declare const STATION_TOOLS: ({
455
475
  seedAlbums?: undefined;
456
476
  songsPerAlbum?: undefined;
457
477
  songSeconds?: undefined;
478
+ parallel?: undefined;
479
+ albumsPerDay?: undefined;
480
+ newArtistChance?: undefined;
481
+ genreOrder?: undefined;
458
482
  state?: undefined;
459
483
  reason?: undefined;
460
484
  open?: undefined;
@@ -535,6 +559,10 @@ export declare const STATION_TOOLS: ({
535
559
  seedAlbums?: undefined;
536
560
  songsPerAlbum?: undefined;
537
561
  songSeconds?: undefined;
562
+ parallel?: undefined;
563
+ albumsPerDay?: undefined;
564
+ newArtistChance?: undefined;
565
+ genreOrder?: undefined;
538
566
  state?: undefined;
539
567
  reason?: undefined;
540
568
  open?: undefined;
@@ -632,6 +660,10 @@ export declare const STATION_TOOLS: ({
632
660
  seedAlbums?: undefined;
633
661
  songsPerAlbum?: undefined;
634
662
  songSeconds?: undefined;
663
+ parallel?: undefined;
664
+ albumsPerDay?: undefined;
665
+ newArtistChance?: undefined;
666
+ genreOrder?: undefined;
635
667
  state?: undefined;
636
668
  reason?: undefined;
637
669
  open?: undefined;
@@ -738,6 +770,24 @@ export declare const STATION_TOOLS: ({
738
770
  songSeconds: {
739
771
  type: string;
740
772
  };
773
+ parallel: {
774
+ type: string;
775
+ };
776
+ albumsPerDay: {
777
+ type: string;
778
+ description: string;
779
+ };
780
+ newArtistChance: {
781
+ type: string;
782
+ description: string;
783
+ };
784
+ genreOrder: {
785
+ type: string;
786
+ items: {
787
+ readonly type: "string";
788
+ };
789
+ description: string;
790
+ };
741
791
  state?: undefined;
742
792
  reason?: undefined;
743
793
  open?: undefined;
@@ -798,6 +848,10 @@ export declare const STATION_TOOLS: ({
798
848
  seedAlbums?: undefined;
799
849
  songsPerAlbum?: undefined;
800
850
  songSeconds?: undefined;
851
+ parallel?: undefined;
852
+ albumsPerDay?: undefined;
853
+ newArtistChance?: undefined;
854
+ genreOrder?: undefined;
801
855
  server: {
802
856
  type: "string";
803
857
  description: string;
@@ -887,6 +941,10 @@ export declare const STATION_TOOLS: ({
887
941
  seedAlbums?: undefined;
888
942
  songsPerAlbum?: undefined;
889
943
  songSeconds?: undefined;
944
+ parallel?: undefined;
945
+ albumsPerDay?: undefined;
946
+ newArtistChance?: undefined;
947
+ genreOrder?: undefined;
890
948
  state?: undefined;
891
949
  reason?: undefined;
892
950
  open?: undefined;
@@ -978,6 +1036,10 @@ export declare const STATION_TOOLS: ({
978
1036
  seedAlbums?: undefined;
979
1037
  songsPerAlbum?: undefined;
980
1038
  songSeconds?: undefined;
1039
+ parallel?: undefined;
1040
+ albumsPerDay?: undefined;
1041
+ newArtistChance?: undefined;
1042
+ genreOrder?: undefined;
981
1043
  state?: undefined;
982
1044
  reason?: undefined;
983
1045
  open?: undefined;
@@ -118,7 +118,10 @@ export const STATION_TOOLS = [
118
118
  sound: { ...STRING, description: "The music in words: instruments, tempo, mood. Name no real artist." },
119
119
  voice: STRING, bio: STRING, themes: STRING, enabled: { type: "boolean" }, publish: { type: "boolean" }, holder: STRING,
120
120
  monthlySeconds: { type: "integer" }, everyDays: { type: "integer" }, seedAlbums: { type: "integer" },
121
- songsPerAlbum: { type: "integer" }, songSeconds: { type: "integer" },
121
+ songsPerAlbum: { type: "integer" }, songSeconds: { type: "integer" }, parallel: { type: "integer" },
122
+ albumsPerDay: { type: "integer", description: "After the first sweep: new albums a day, by a random artist or a newly invented one." },
123
+ newArtistChance: { type: "number", description: "0-1: how often the day's album is by a new artist." },
124
+ genreOrder: { type: "array", items: STRING, description: "Genres whose artists get albums first, in order." },
122
125
  },
123
126
  },
124
127
  },
@@ -237,7 +240,7 @@ export async function callStationTool(name, args, options = {}) {
237
240
  const key = typeof args["key"] === "string" && /^[a-f0-9]{10}$/.test(args["key"]) ? args["key"] : "";
238
241
  const answer = action === "status" ? await call("GET", "/api/v1/stations/label")
239
242
  : 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"]))
243
+ : action === "configure" ? await call("POST", "/api/v1/stations/label", pick(["holder", "monthlySeconds", "everyDays", "seedAlbums", "songsPerAlbum", "songSeconds", "parallel", "genreOrder", "albumsPerDay", "newArtistChance", "enabled", "publish"]))
241
244
  : action === "run" ? await call("POST", "/api/v1/stations/label/run", {})
242
245
  : action === "resume" ? await call("POST", "/api/v1/stations/label", { resume: true })
243
246
  : !key ? null
@@ -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),\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 roster: three artists in each of Rock, Hip-Hop, Synthwave, Country,\n Metal and Pop (nixamp station label lists their keys)\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 1]\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.";
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 roster: three artists in each of Rock, Hip-Hop, Synthwave, Country,\n Metal and Pop (nixamp station label lists their keys)\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 1]\n [--songs 8] [--seconds 150] [--on|--off] [--public|--unlisted]\n [--parallel 4] [--genres \"Punk,Blues,Country\"] songs made at once; genres first\n [--per-day 1] [--new-artists 0.25] after the first sweep: albums a day, share by new artists\n [--videos|--no-videos] a music video per song and a TV station of them\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.";
@@ -46,6 +46,9 @@ export const USAGE = `nixamp station — make a collection into a station
46
46
  nixamp station label drop KEY [--files]
47
47
  nixamp station label config [--holder "Profullstack, Inc."] [--budget SECONDS] [--every-days 30] [--seed-albums 1]
48
48
  [--songs 8] [--seconds 150] [--on|--off] [--public|--unlisted]
49
+ [--parallel 4] [--genres "Punk,Blues,Country"] songs made at once; genres first
50
+ [--per-day 1] [--new-artists 0.25] after the first sweep: albums a day, share by new artists
51
+ [--videos|--no-videos] a music video per song and a TV station of them
49
52
  nixamp station label run record whatever is due now
50
53
  nixamp station label resume after the provider ran out of credit
51
54
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.51.0",
3
+ "version": "0.52.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",