nixamp 0.50.1 → 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.
- package/dist/ai-label.d.ts +70 -3
- package/dist/ai-label.js +459 -29
- package/dist/label-routes.js +2 -1
- package/dist/main.js +9 -0
- package/dist/mcp.js +42 -0
- package/dist/music-cli.d.ts +2 -0
- package/dist/music-cli.js +109 -0
- package/dist/music-client.d.ts +62 -0
- package/dist/music-client.js +73 -0
- package/dist/station-cli.js +8 -1
- package/dist/station-mcp.d.ts +62 -0
- package/dist/station-mcp.js +5 -2
- package/dist/station-usage.d.ts +1 -1
- package/dist/station-usage.js +6 -3
- package/dist/track-facts.js +1 -1
- package/package.json +1 -1
- package/src/ai-label.ts +464 -29
- package/src/label-routes.ts +2 -1
- package/src/main.ts +8 -0
- package/src/mcp.ts +38 -0
- package/src/music-cli.ts +86 -0
- package/src/music-client.ts +86 -0
- package/src/station-cli.ts +8 -1
- package/src/station-mcp.ts +5 -2
- package/src/station-usage.ts +6 -3
- package/src/track-facts.ts +1 -1
- package/web/dist/assets/{ar-CC_uYRlC.js → ar-DMx2aeAO.js} +1 -1
- package/web/dist/assets/{cs-k5C-jvfX.js → cs-Ddu0q3kG.js} +1 -1
- package/web/dist/assets/{da-CdBzU_fF.js → da-CZ9gg_pb.js} +1 -1
- package/web/dist/assets/{de-B-UWa0W7.js → de-CgJX4xg2.js} +1 -1
- package/web/dist/assets/{es-Dod8aCau.js → es-Cp5ryDrJ.js} +1 -1
- package/web/dist/assets/{fi-C36bBPFW.js → fi-D912EKux.js} +1 -1
- package/web/dist/assets/{fr-DWUxs2qv.js → fr-FE_K0MPb.js} +1 -1
- package/web/dist/assets/{hi-DjLqhfs8.js → hi-BdYtQs1S.js} +1 -1
- package/web/dist/assets/{hu-CgsOlcq8.js → hu-oh5tEg0p.js} +1 -1
- package/web/dist/assets/{id-ZA8yTWX9.js → id-NbCUWeB1.js} +1 -1
- package/web/dist/assets/{it-DfFhS34g.js → it-CiHbKZ6Y.js} +1 -1
- package/web/dist/assets/{main-CjgQp9um.js → main-8wV87D-g.js} +2 -2
- package/web/dist/assets/{nl-C257i24U.js → nl-nLHK_AQ-.js} +1 -1
- package/web/dist/assets/{radio-CDUEoM0y.js → radio-BmHvBkVy.js} +1 -1
- package/web/dist/assets/{ru--30I7BNC.js → ru-BDdAS0Vv.js} +1 -1
- package/web/dist/assets/{sv-CxzBeXfk.js → sv-COweXM56.js} +1 -1
- package/web/dist/assets/{uk-DXHpMk4i.js → uk-BJFzR1LS.js} +1 -1
- package/web/dist/assets/{vi-Dg2LT4x3.js → vi-qg2DJ6cR.js} +1 -1
- package/web/dist/assets/{zh-D3pvejo6.js → zh-DUWOKDkb.js} +1 -1
- package/web/dist/index.html +2 -1
- package/web/dist/radio.html +1 -1
- package/web/dist/sw.js +20 -20
package/dist/main.js
CHANGED
|
@@ -9,6 +9,7 @@ import { t as uiMessage, i18n, terminalUiLanguage, uiLanguage } from "./i18n.js"
|
|
|
9
9
|
*/
|
|
10
10
|
import { USAGE as STATION_USAGE } from "./station-usage.js";
|
|
11
11
|
import { LIBRARY_COMMANDS, LIBRARY_USAGE } from "./library-usage.js";
|
|
12
|
+
import { MUSIC_USAGE } from "./music-cli.js";
|
|
12
13
|
import { createApp, themes } from "@profullstack/hqtui";
|
|
13
14
|
import { fileURLToPath } from "node:url";
|
|
14
15
|
import { resolve } from "node:path";
|
|
@@ -209,6 +210,7 @@ keeps serving its browser remote. One per user.
|
|
|
209
210
|
nixamp attach put the player back in front of it
|
|
210
211
|
nixamp admin who is connected, and re-stream to them
|
|
211
212
|
nixamp station make a folder into a station others can join (see: nixamp station help)
|
|
213
|
+
nixamp music the AI label's songs as files: catalog, buy (10¢, $1, $2), what you bought (nixamp music help)
|
|
212
214
|
|
|
213
215
|
From inside the player, d hands the music to a daemon without stopping it.
|
|
214
216
|
`,
|
|
@@ -374,6 +376,8 @@ export function helpFor(topic) {
|
|
|
374
376
|
return STATION_USAGE;
|
|
375
377
|
if (topic === "library")
|
|
376
378
|
return LIBRARY_USAGE;
|
|
379
|
+
if (topic === "music")
|
|
380
|
+
return MUSIC_USAGE;
|
|
377
381
|
return (topic ? TOPICS[topic] : undefined) ?? HELP;
|
|
378
382
|
}
|
|
379
383
|
/**
|
|
@@ -531,6 +535,11 @@ export async function main() {
|
|
|
531
535
|
process.exitCode = await runDaemon(rest);
|
|
532
536
|
return;
|
|
533
537
|
}
|
|
538
|
+
if (first === "music") {
|
|
539
|
+
const { music } = await import("./music-cli.js");
|
|
540
|
+
process.exitCode = await music(rest);
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
534
543
|
if (first === "station" || first === "stations") {
|
|
535
544
|
const { station } = await import("./station-cli.js");
|
|
536
545
|
process.exitCode = await station(rest);
|
package/dist/mcp.js
CHANGED
|
@@ -218,6 +218,29 @@ export const TOOLS = [
|
|
|
218
218
|
required: ["server"],
|
|
219
219
|
},
|
|
220
220
|
},
|
|
221
|
+
{
|
|
222
|
+
name: "music_catalog",
|
|
223
|
+
description: "The AI label's catalogue: genres, artists, albums and songs (with lyrics), and each genre's free live station. Read from the server that holds the music, live.streaming by default.",
|
|
224
|
+
inputSchema: { type: "object", properties: { server: { ...STRING, description: "The music server's https address (default https://live.streaming.pit.world)." } } },
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
name: "music_buy",
|
|
228
|
+
description: "Buy AI label music as files (a song 10¢, an album $1, everything by an artist $2) with crypto via CoinPay. Answers the page to pay on and the receipt link where the zip (MP3s with cover art and lyrics) is downloaded once paid. Pass a nixamp.com/buy link, or server + kind + artist (+ album, + n).",
|
|
229
|
+
inputSchema: {
|
|
230
|
+
type: "object",
|
|
231
|
+
properties: {
|
|
232
|
+
link: { ...STRING, description: "A nixamp.com/buy?... link." },
|
|
233
|
+
server: STRING, kind: { type: "string", enum: ["song", "album", "artist"] }, artist: STRING, album: STRING, n: { type: "integer" },
|
|
234
|
+
email: { ...STRING, description: "Where the receipt goes; the signed-in account's email by default." },
|
|
235
|
+
coin: { ...STRING, description: "USDC_POL (default), USDC_SOL, SOL, POL, ETH, BTC..." },
|
|
236
|
+
},
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
name: "music_purchases",
|
|
241
|
+
description: "The AI label music this account bought, with a fresh download link for each zip. Needs `nixamp login` with the email used to buy.",
|
|
242
|
+
inputSchema: { type: "object", properties: {} },
|
|
243
|
+
},
|
|
221
244
|
...STATION_TOOLS,
|
|
222
245
|
...PUBLIC_TOOLS,
|
|
223
246
|
...LIBRARY_TOOLS,
|
|
@@ -260,6 +283,25 @@ export async function callTool(name, args, options = {}) {
|
|
|
260
283
|
...(options.session?.site ? { site: options.session.site } : {}),
|
|
261
284
|
});
|
|
262
285
|
}
|
|
286
|
+
// The AI label's music: the catalogue and buying need no sign-in; what was bought does.
|
|
287
|
+
if (name === "music_catalog" || name === "music_buy" || name === "music_purchases") {
|
|
288
|
+
const shop = await import("./music-client.js");
|
|
289
|
+
try {
|
|
290
|
+
if (name === "music_catalog")
|
|
291
|
+
return text(JSON.stringify(await shop.catalogue(typeof args["server"] === "string" && args["server"] ? args["server"] : undefined, options.fetcher), null, 2));
|
|
292
|
+
if (name === "music_purchases")
|
|
293
|
+
return text(JSON.stringify(await shop.purchases(options.fetcher), null, 2));
|
|
294
|
+
const link = typeof args["link"] === "string" ? shop.fromBuyLink(args["link"]) : null;
|
|
295
|
+
if (typeof args["link"] === "string" && !link)
|
|
296
|
+
return failed("that is not a nixamp.com/buy link");
|
|
297
|
+
const pick = (keys) => Object.fromEntries(keys.filter((key) => args[key] !== undefined).map((key) => [key, args[key]]));
|
|
298
|
+
const made = await shop.buy({ ...(link ?? pick(["server", "kind", "artist", "album", "n"])), ...(link?.["n"] ? { n: Number(link["n"]) } : {}), coin: typeof args["coin"] === "string" ? args["coin"] : "USDC_POL", ...pick(["email"]) }, options.fetcher);
|
|
299
|
+
return text(`Pay here: ${made.url}\nReceipt and download, once paid: ${made.receipt}`);
|
|
300
|
+
}
|
|
301
|
+
catch (error) {
|
|
302
|
+
return failed(error.message);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
263
305
|
// Stations first: on this machine they need the daemon, not a sign-in.
|
|
264
306
|
if (isStationTool(name) || isLibraryTool(name)) {
|
|
265
307
|
const local = { ...(options.fetcher ? { fetcher: options.fetcher } : {}), ...(options.session !== undefined ? { session: options.session } : {}), ...(options.stations ?? {}) };
|
|
@@ -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";
|
package/dist/station-cli.js
CHANGED
|
@@ -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));
|
package/dist/station-mcp.d.ts
CHANGED
|
@@ -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;
|
package/dist/station-mcp.js
CHANGED
|
@@ -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
|
package/dist/station-usage.d.ts
CHANGED
|
@@ -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
|
|
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.";
|
package/dist/station-usage.js
CHANGED
|
@@ -38,14 +38,17 @@ export const USAGE = `nixamp station — make a collection into a station
|
|
|
38
38
|
|
|
39
39
|
AI label (AI artists who release albums on their own; a station per genre; the catalogue at /label):
|
|
40
40
|
nixamp station label artists, albums, this month's seconds, what it is waiting for
|
|
41
|
-
nixamp station label sign --roster the
|
|
42
|
-
|
|
41
|
+
nixamp station label sign --roster the roster: three artists in each of Rock, Hip-Hop, Synthwave, Country,
|
|
42
|
+
Metal and Pop (nixamp station label lists their keys)
|
|
43
43
|
nixamp station label sign (--preset KEY | --name NAME --genre GENRE --sound TEXT [--voice TEXT] [--bio TEXT] [--themes TEXT])
|
|
44
44
|
nixamp station label album KEY plan the artist's next album now and record it
|
|
45
45
|
nixamp station label set KEY [--sound TEXT] [--voice TEXT] [--bio TEXT] [--themes TEXT] [--off|--on]
|
|
46
46
|
nixamp station label drop KEY [--files]
|
|
47
|
-
nixamp station label config [--holder "Profullstack, Inc."] [--budget SECONDS] [--every-days 30] [--seed-albums
|
|
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/dist/track-facts.js
CHANGED
|
@@ -130,7 +130,7 @@ export function factsFor(input) {
|
|
|
130
130
|
const copyright = tags["copyright"] || tags["tcop"] || "";
|
|
131
131
|
let rights;
|
|
132
132
|
if (copyright) {
|
|
133
|
-
rights = { holder: copyright.replace(/^(\(c\)
|
|
133
|
+
rights = { holder: copyright.replace(/^((\(c\)|\(p\)|©|℗|copyright)\s*)+/i, "").slice(0, 200), basis: "", note: "", license: holder?.license ?? "", source: "tag" };
|
|
134
134
|
}
|
|
135
135
|
else if (holder) {
|
|
136
136
|
rights = { holder: holder.name, basis: station.rights?.basis ?? "", note: "", license: holder.license, source: "holder" };
|