nixamp 0.29.1 → 0.30.1
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/channels.js +4 -1
- package/dist/directory.d.ts +3 -1
- package/dist/directory.js +8 -2
- package/dist/main.d.ts +0 -8
- package/dist/main.js +9 -0
- package/dist/server.d.ts +27 -1
- package/dist/server.js +162 -6
- package/dist/station-cli.d.ts +3 -0
- package/dist/station-cli.js +201 -0
- package/dist/station-page.d.ts +5 -0
- package/dist/station-page.js +137 -0
- package/dist/station-routes.d.ts +60 -0
- package/dist/station-routes.js +249 -0
- package/dist/station-runner.d.ts +67 -0
- package/dist/station-runner.js +219 -0
- package/dist/station-usage.d.ts +2 -0
- package/dist/station-usage.js +20 -0
- package/dist/stations.d.ts +148 -0
- package/dist/stations.js +331 -0
- package/package.json +1 -1
- package/src/channels.ts +4 -1
- package/src/directory.ts +10 -2
- package/src/main.ts +8 -0
- package/src/server.ts +169 -7
- package/src/station-cli.ts +195 -0
- package/src/station-page.ts +144 -0
- package/src/station-routes.ts +301 -0
- package/src/station-runner.ts +244 -0
- package/src/station-usage.ts +20 -0
- package/src/stations.ts +407 -0
- package/web/dist/assets/{ar-D6_ZFDan.js → ar-CTUDxy1J.js} +1 -1
- package/web/dist/assets/{cs-DEPGOGEV.js → cs-m4JTaO3o.js} +1 -1
- package/web/dist/assets/{da-CQw7SPC9.js → da-DHnJElYB.js} +1 -1
- package/web/dist/assets/{de-OWnobbah.js → de-DkxPohnf.js} +1 -1
- package/web/dist/assets/{es-qMpyH9bM.js → es-oY_G83d7.js} +1 -1
- package/web/dist/assets/{fi-DpqqwJTD.js → fi-fCO8XRdg.js} +1 -1
- package/web/dist/assets/{fr-BiO_ceen.js → fr-CSupf-IN.js} +1 -1
- package/web/dist/assets/{hi-D-q5wZuY.js → hi-Dve7PJsC.js} +1 -1
- package/web/dist/assets/{hls-3VKVEQE3-D2W4K8VR.js → hls-3VKVEQE3-D5-AkKSR.js} +1 -1
- package/web/dist/assets/{hu-FIjHyhVC.js → hu-D1KYamzV.js} +1 -1
- package/web/dist/assets/{id-gmB6lu56.js → id-Cofp_4aQ.js} +1 -1
- package/web/dist/assets/{index-UmGvzuun.js → index-7mCQZGNF.js} +2 -2
- package/web/dist/assets/{it-Dxfo713A.js → it-62iXQWQD.js} +1 -1
- package/web/dist/assets/{mpegts-BT4XGr_E.js → mpegts-BT5ESacM.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-CpZnlq0W.js → mpegts-LO6RVLD6-BgHd8PFT.js} +1 -1
- package/web/dist/assets/{nl-mk5eN3Io.js → nl-CzRRG-CN.js} +1 -1
- package/web/dist/assets/{ru-B2psU0kN.js → ru-OykC1LJQ.js} +1 -1
- package/web/dist/assets/{sv-Cg6waoNZ.js → sv-B8FXBDHm.js} +1 -1
- package/web/dist/assets/{uk-BcFkukLd.js → uk-DhAJBjwH.js} +1 -1
- package/web/dist/assets/{vi-CnqIC1Sz.js → vi-CdBSRReC.js} +1 -1
- package/web/dist/assets/{zh-CvnXIFvN.js → zh-wGWNyBEZ.js} +1 -1
- package/web/dist/index.html +1 -1
- package/web/dist/sw.js +22 -22
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import { EXCLUDE_AFTER, OFFLINE_RETRY_MS, RECOVERY_DELAYS_MS, StationError, channelOf, insideRoots, shuffled, stationOfChannel, } from "./stations.js";
|
|
2
|
+
/** A run shorter than this that ends badly does not reset the backoff. */
|
|
3
|
+
const HEALTHY_RUN_MS = 60_000;
|
|
4
|
+
export class StationRunner {
|
|
5
|
+
deps;
|
|
6
|
+
attempts = new Map();
|
|
7
|
+
timers = new Map();
|
|
8
|
+
startedAt = new Map();
|
|
9
|
+
stopping = new Set();
|
|
10
|
+
starting = new Set();
|
|
11
|
+
errors = new Map();
|
|
12
|
+
listening = new Map();
|
|
13
|
+
constructor(deps) {
|
|
14
|
+
this.deps = deps;
|
|
15
|
+
}
|
|
16
|
+
now() {
|
|
17
|
+
return this.deps.now?.() ?? Date.now();
|
|
18
|
+
}
|
|
19
|
+
log(line) {
|
|
20
|
+
this.deps.log?.(line);
|
|
21
|
+
}
|
|
22
|
+
/** The station's files that are still where they were, inside the library. */
|
|
23
|
+
playable(station) {
|
|
24
|
+
const roots = this.deps.roots();
|
|
25
|
+
return station.items.filter((file) => insideRoots(file, roots));
|
|
26
|
+
}
|
|
27
|
+
/** Wanted on, and put on. */
|
|
28
|
+
async start(id) {
|
|
29
|
+
const station = this.deps.store.get(id);
|
|
30
|
+
if (!station)
|
|
31
|
+
throw new StationError("not_found", "no such station", 404);
|
|
32
|
+
if (this.playable(station).length === 0) {
|
|
33
|
+
throw new StationError("nothing_playable", "none of this station's files can be found in the library", 409);
|
|
34
|
+
}
|
|
35
|
+
this.deps.store.setDesired(id, "running", this.now());
|
|
36
|
+
this.cancelRecovery(id);
|
|
37
|
+
this.attempts.delete(id);
|
|
38
|
+
await this.launch(id);
|
|
39
|
+
return this.deps.store.get(id);
|
|
40
|
+
}
|
|
41
|
+
/** Wanted off, and taken off. Stays off across restarts. */
|
|
42
|
+
stop(id) {
|
|
43
|
+
const station = this.deps.store.setDesired(id, "stopped", this.now());
|
|
44
|
+
this.cancelRecovery(id);
|
|
45
|
+
this.attempts.delete(id);
|
|
46
|
+
this.errors.delete(id);
|
|
47
|
+
const channelId = channelOf(id);
|
|
48
|
+
if (this.deps.channels.has(channelId)) {
|
|
49
|
+
this.stopping.add(channelId);
|
|
50
|
+
this.deps.channels.stop(channelId);
|
|
51
|
+
}
|
|
52
|
+
return station;
|
|
53
|
+
}
|
|
54
|
+
/** The next track, now. */
|
|
55
|
+
skip(id) {
|
|
56
|
+
if (!this.deps.store.get(id))
|
|
57
|
+
throw new StationError("not_found", "no such station", 404);
|
|
58
|
+
if (!this.deps.channels.skip(channelOf(id)))
|
|
59
|
+
throw new StationError("not_live", "the station is not playing", 409);
|
|
60
|
+
}
|
|
61
|
+
/** Stopped and forgotten. */
|
|
62
|
+
remove(id) {
|
|
63
|
+
if (!this.deps.store.get(id))
|
|
64
|
+
throw new StationError("not_found", "no such station", 404);
|
|
65
|
+
this.stop(id);
|
|
66
|
+
this.revoke(id);
|
|
67
|
+
this.deps.store.remove(id);
|
|
68
|
+
}
|
|
69
|
+
/** Put back every station that was running when this server last stopped. */
|
|
70
|
+
restore() {
|
|
71
|
+
for (const station of this.deps.store.list()) {
|
|
72
|
+
if (station.desired !== "running")
|
|
73
|
+
continue;
|
|
74
|
+
this.log(` Putting station "${station.name}" back on the air.`);
|
|
75
|
+
void this.launch(station.id);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async launch(id) {
|
|
79
|
+
const station = this.deps.store.get(id);
|
|
80
|
+
if (!station || station.desired !== "running")
|
|
81
|
+
return false;
|
|
82
|
+
const channelId = channelOf(id);
|
|
83
|
+
if (this.deps.channels.has(channelId) || this.starting.has(id))
|
|
84
|
+
return true;
|
|
85
|
+
const files = this.playable(station);
|
|
86
|
+
if (files.length === 0) {
|
|
87
|
+
this.errors.set(id, "none of this station's files can be found in the library");
|
|
88
|
+
this.scheduleRecovery(id);
|
|
89
|
+
return false;
|
|
90
|
+
}
|
|
91
|
+
this.starting.add(id);
|
|
92
|
+
const store = this.deps.store;
|
|
93
|
+
try {
|
|
94
|
+
const ok = await this.deps.pull(channelId, station.name, files, {
|
|
95
|
+
audioBitrate: station.bitrate,
|
|
96
|
+
rotation: {
|
|
97
|
+
repeat: station.repeat,
|
|
98
|
+
passOver: true,
|
|
99
|
+
// Read at every pass, so turning shuffle on or off lands then.
|
|
100
|
+
order: (length) => (store.get(id)?.shuffle ? shuffled(length) : [...Array(length).keys()]),
|
|
101
|
+
// Read at every track, so an edit lands on the next one.
|
|
102
|
+
current: () => {
|
|
103
|
+
const now = store.get(id);
|
|
104
|
+
return now ? this.playable(now) : [];
|
|
105
|
+
},
|
|
106
|
+
excluded: (file) => store.isExcluded(id, file),
|
|
107
|
+
onFailure: ({ file, reason }) => {
|
|
108
|
+
this.errors.set(id, reason);
|
|
109
|
+
if (store.recordFailure(id, file)) {
|
|
110
|
+
this.log(` Station "${station.name}": left out a file that failed ${EXCLUDE_AFTER} times.`);
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
if (!ok) {
|
|
116
|
+
this.errors.set(id, "the station could not start");
|
|
117
|
+
this.scheduleRecovery(id);
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
this.startedAt.set(id, this.now());
|
|
121
|
+
this.deps.channels.keep(channelId);
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
catch (error) {
|
|
125
|
+
this.errors.set(id, error instanceof Error ? error.message : String(error));
|
|
126
|
+
this.scheduleRecovery(id);
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
finally {
|
|
130
|
+
this.starting.delete(id);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
/** Told by the server when any channel ends. */
|
|
134
|
+
onChannelEnd(info) {
|
|
135
|
+
const id = stationOfChannel(info.id);
|
|
136
|
+
if (!id)
|
|
137
|
+
return;
|
|
138
|
+
if (this.stopping.delete(info.id))
|
|
139
|
+
return;
|
|
140
|
+
const station = this.deps.store.get(id);
|
|
141
|
+
if (!station || station.desired !== "running")
|
|
142
|
+
return;
|
|
143
|
+
// A list that is not repeated played through: that is the end of it,
|
|
144
|
+
// which is the owner's choice, not a failure.
|
|
145
|
+
if (info.ended && !station.repeat) {
|
|
146
|
+
this.deps.store.setDesired(id, "stopped", this.now());
|
|
147
|
+
this.errors.delete(id);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const ran = this.now() - (this.startedAt.get(id) ?? 0);
|
|
151
|
+
if (ran >= HEALTHY_RUN_MS)
|
|
152
|
+
this.attempts.delete(id);
|
|
153
|
+
this.errors.set(id, info.error || "the station stopped");
|
|
154
|
+
this.scheduleRecovery(id);
|
|
155
|
+
}
|
|
156
|
+
scheduleRecovery(id) {
|
|
157
|
+
if (this.timers.has(id))
|
|
158
|
+
return;
|
|
159
|
+
const attempt = this.attempts.get(id) ?? 0;
|
|
160
|
+
const delay = attempt < RECOVERY_DELAYS_MS.length ? RECOVERY_DELAYS_MS[attempt] : OFFLINE_RETRY_MS;
|
|
161
|
+
this.attempts.set(id, attempt + 1);
|
|
162
|
+
const timer = setTimeout(() => {
|
|
163
|
+
this.timers.delete(id);
|
|
164
|
+
void this.launch(id);
|
|
165
|
+
}, delay);
|
|
166
|
+
timer.unref?.();
|
|
167
|
+
this.timers.set(id, timer);
|
|
168
|
+
}
|
|
169
|
+
cancelRecovery(id) {
|
|
170
|
+
const timer = this.timers.get(id);
|
|
171
|
+
if (timer)
|
|
172
|
+
clearTimeout(timer);
|
|
173
|
+
this.timers.delete(id);
|
|
174
|
+
}
|
|
175
|
+
observed(id) {
|
|
176
|
+
const station = this.deps.store.get(id);
|
|
177
|
+
if (!station)
|
|
178
|
+
return "stopped";
|
|
179
|
+
if (station.desired === "stopped")
|
|
180
|
+
return "stopped";
|
|
181
|
+
if (this.starting.has(id))
|
|
182
|
+
return "starting";
|
|
183
|
+
const info = this.deps.channels.info(channelOf(id));
|
|
184
|
+
if (info)
|
|
185
|
+
return info.redials ? "recovering" : "live";
|
|
186
|
+
return (this.attempts.get(id) ?? 0) <= RECOVERY_DELAYS_MS.length && this.timers.has(id) ? "recovering" : "offline";
|
|
187
|
+
}
|
|
188
|
+
error(id) {
|
|
189
|
+
return this.errors.get(id) ?? "";
|
|
190
|
+
}
|
|
191
|
+
/** A listener on a station's link, ended if the link is revoked. */
|
|
192
|
+
track(id, listener) {
|
|
193
|
+
let set = this.listening.get(id);
|
|
194
|
+
if (!set) {
|
|
195
|
+
set = new Set();
|
|
196
|
+
this.listening.set(id, set);
|
|
197
|
+
}
|
|
198
|
+
set.add(listener);
|
|
199
|
+
return () => set.delete(listener);
|
|
200
|
+
}
|
|
201
|
+
listeners(id) {
|
|
202
|
+
return this.listening.get(id)?.size ?? 0;
|
|
203
|
+
}
|
|
204
|
+
/** Everybody listening through a link that no longer works is let go. */
|
|
205
|
+
revoke(id) {
|
|
206
|
+
const set = this.listening.get(id);
|
|
207
|
+
if (!set)
|
|
208
|
+
return 0;
|
|
209
|
+
const count = set.size;
|
|
210
|
+
for (const one of [...set]) {
|
|
211
|
+
try {
|
|
212
|
+
one.end();
|
|
213
|
+
}
|
|
214
|
+
catch { /* already gone */ }
|
|
215
|
+
}
|
|
216
|
+
set.clear();
|
|
217
|
+
return count;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
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 | --items 0,1,2) [--shuffle] [--no-repeat] [--bitrate 128]\n nixamp station show ID\n nixamp station status ID\n nixamp station start ID\n nixamp station stop ID\n nixamp station skip ID\n nixamp station retry ID try every left-out file again\n nixamp station publish ID --basis owned|permission|licensed|public-domain [--note TEXT] [--unlisted] [--no-list]\n nixamp station unpublish ID [--private]\n nixamp station link ID [--rotate]\n nixamp station delete ID\n\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.";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** `nixamp station` usage, apart so `nixamp help station` does not load the client. */
|
|
2
|
+
export const USAGE = `nixamp station — make a collection into a station
|
|
3
|
+
|
|
4
|
+
nixamp station list
|
|
5
|
+
nixamp station create --name NAME (--folder PATH | --items 0,1,2) [--shuffle] [--no-repeat] [--bitrate 128]
|
|
6
|
+
nixamp station show ID
|
|
7
|
+
nixamp station status ID
|
|
8
|
+
nixamp station start ID
|
|
9
|
+
nixamp station stop ID
|
|
10
|
+
nixamp station skip ID
|
|
11
|
+
nixamp station retry ID try every left-out file again
|
|
12
|
+
nixamp station publish ID --basis owned|permission|licensed|public-domain [--note TEXT] [--unlisted] [--no-list]
|
|
13
|
+
nixamp station unpublish ID [--private]
|
|
14
|
+
nixamp station link ID [--rotate]
|
|
15
|
+
nixamp station delete ID
|
|
16
|
+
|
|
17
|
+
--json machine-readable output
|
|
18
|
+
--url, --key another server, as with \`nixamp admin\`
|
|
19
|
+
|
|
20
|
+
A new station is private and stopped. Starting it does not publish it.`;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
export type Visibility = "private" | "unlisted" | "public";
|
|
2
|
+
export type Desired = "running" | "stopped";
|
|
3
|
+
export type Observed = "stopped" | "starting" | "live" | "recovering" | "offline";
|
|
4
|
+
/** Why the owner may broadcast this publicly. A declaration, not a clearance. */
|
|
5
|
+
export type RightsBasis = "owned" | "permission" | "licensed" | "public-domain";
|
|
6
|
+
export declare const RIGHTS_BASES: readonly RightsBasis[];
|
|
7
|
+
export interface Rights {
|
|
8
|
+
basis: RightsBasis;
|
|
9
|
+
note: string;
|
|
10
|
+
declaredAt: number;
|
|
11
|
+
declaredBy: string;
|
|
12
|
+
}
|
|
13
|
+
export interface Station {
|
|
14
|
+
id: string;
|
|
15
|
+
name: string;
|
|
16
|
+
description: string;
|
|
17
|
+
/** Files on this machine, absolute, in the station's own order. */
|
|
18
|
+
items: string[];
|
|
19
|
+
shuffle: boolean;
|
|
20
|
+
repeat: boolean;
|
|
21
|
+
visibility: Visibility;
|
|
22
|
+
/** In the public directory. Only a public station can be. */
|
|
23
|
+
listed: boolean;
|
|
24
|
+
desired: Desired;
|
|
25
|
+
/** Opens this station's stream and nothing else. Rotated to revoke a link. */
|
|
26
|
+
listenToken: string;
|
|
27
|
+
/** Audio bitrate of the station's rendition, in kbps. */
|
|
28
|
+
bitrate: number;
|
|
29
|
+
rights: Rights | null;
|
|
30
|
+
/** nixamp.com account that made it, "" when made with the control key. */
|
|
31
|
+
owner: string;
|
|
32
|
+
/** Files that would not play, and how many times in a row. */
|
|
33
|
+
failures: Record<string, number>;
|
|
34
|
+
/** What the caller said made this one, so asking twice makes it once. */
|
|
35
|
+
idempotencyKey: string;
|
|
36
|
+
createdAt: number;
|
|
37
|
+
updatedAt: number;
|
|
38
|
+
}
|
|
39
|
+
/** Times an item may fail in a row before rotation leaves it out. */
|
|
40
|
+
export declare const EXCLUDE_AFTER = 3;
|
|
41
|
+
export declare const DEFAULT_BITRATE = 128;
|
|
42
|
+
export declare const BITRATES: number[];
|
|
43
|
+
/** A station's channel id. */
|
|
44
|
+
export declare function channelOf(id: string): string;
|
|
45
|
+
/** The station a channel id belongs to, or "" for any other channel. */
|
|
46
|
+
export declare function stationOfChannel(channelId: string): string;
|
|
47
|
+
export declare function tokensMatch(offered: string, held: string): boolean;
|
|
48
|
+
/** A path inside one of these roots, after symlinks: never outside the library. */
|
|
49
|
+
export declare function insideRoots(path: string, roots: string[]): boolean;
|
|
50
|
+
export declare class StationError extends Error {
|
|
51
|
+
readonly code: string;
|
|
52
|
+
readonly status: number;
|
|
53
|
+
constructor(code: string, message: string, status?: number);
|
|
54
|
+
}
|
|
55
|
+
export interface StationInput {
|
|
56
|
+
name?: unknown;
|
|
57
|
+
description?: unknown;
|
|
58
|
+
items?: unknown;
|
|
59
|
+
shuffle?: unknown;
|
|
60
|
+
repeat?: unknown;
|
|
61
|
+
bitrate?: unknown;
|
|
62
|
+
}
|
|
63
|
+
/** The stations of one server, kept in one file beside its keys. */
|
|
64
|
+
export declare class StationStore {
|
|
65
|
+
private readonly file;
|
|
66
|
+
private readonly io;
|
|
67
|
+
private stations;
|
|
68
|
+
constructor(file: string, io?: {
|
|
69
|
+
read: (path: string) => string | null;
|
|
70
|
+
write: (path: string, body: string) => void;
|
|
71
|
+
});
|
|
72
|
+
static fileFor(stateDir: string, port: number): string;
|
|
73
|
+
private save;
|
|
74
|
+
list(): Station[];
|
|
75
|
+
get(id: string): Station | undefined;
|
|
76
|
+
/** New, and always private, stopped and unlisted (RAD-02). */
|
|
77
|
+
/** The station a create with this key already made. */
|
|
78
|
+
byIdempotencyKey(key: string): Station | undefined;
|
|
79
|
+
create(input: StationInput, items: string[], owner: string, now?: number, idempotencyKey?: string): Station;
|
|
80
|
+
private bitrateFrom;
|
|
81
|
+
/** Name, words, files and rotation. Who may listen is changed by publish/unpublish. */
|
|
82
|
+
update(id: string, input: StationInput, items: string[] | null, now?: number): Station;
|
|
83
|
+
remove(id: string): boolean;
|
|
84
|
+
setDesired(id: string, desired: Desired, now?: number): Station;
|
|
85
|
+
/**
|
|
86
|
+
* Who may hear it, and whether the directory lists it. Public needs a
|
|
87
|
+
* declared basis; listed needs public. Going private mints a new token, so
|
|
88
|
+
* every link handed out before stops working, and the caller drops the
|
|
89
|
+
* listeners who used one.
|
|
90
|
+
*/
|
|
91
|
+
setAccess(id: string, access: {
|
|
92
|
+
visibility?: unknown;
|
|
93
|
+
listed?: unknown;
|
|
94
|
+
rights?: {
|
|
95
|
+
basis?: unknown;
|
|
96
|
+
note?: unknown;
|
|
97
|
+
} | null;
|
|
98
|
+
}, by: string, now?: number): {
|
|
99
|
+
station: Station;
|
|
100
|
+
revoked: boolean;
|
|
101
|
+
};
|
|
102
|
+
/** A new listen link; every earlier one stops working. */
|
|
103
|
+
rotateToken(id: string, now?: number): Station;
|
|
104
|
+
/** One more failure in a row for this file; true when it is now left out. */
|
|
105
|
+
recordFailure(id: string, file: string): boolean;
|
|
106
|
+
/** Played through: its failures are forgotten. */
|
|
107
|
+
recordSuccess(id: string, file: string): void;
|
|
108
|
+
/** Every left-out file tried again (the owner's "retry"). */
|
|
109
|
+
clearFailures(id: string): Station;
|
|
110
|
+
isExcluded(id: string, file: string): boolean;
|
|
111
|
+
private require;
|
|
112
|
+
}
|
|
113
|
+
/** A station-wide shuffle: one order for everybody, Fisher-Yates. */
|
|
114
|
+
export declare function shuffled(length: number, random?: () => number): number[];
|
|
115
|
+
/**
|
|
116
|
+
* Restart after an unexpected end: 2 s, doubling, at most a minute apart.
|
|
117
|
+
* After the last of these the station is offline but still wanted, and is
|
|
118
|
+
* tried again every OFFLINE_RETRY_MS so a source that comes back is found.
|
|
119
|
+
*/
|
|
120
|
+
export declare const RECOVERY_DELAYS_MS: number[];
|
|
121
|
+
export declare const OFFLINE_RETRY_MS: number;
|
|
122
|
+
/** What a listener with the link may see. Never a path, never a token. */
|
|
123
|
+
export interface PublicStation {
|
|
124
|
+
id: string;
|
|
125
|
+
name: string;
|
|
126
|
+
description: string;
|
|
127
|
+
visibility: Visibility;
|
|
128
|
+
state: Observed;
|
|
129
|
+
nowPlaying: {
|
|
130
|
+
title: string;
|
|
131
|
+
artist: string;
|
|
132
|
+
album: string;
|
|
133
|
+
} | null;
|
|
134
|
+
format: {
|
|
135
|
+
codec: string;
|
|
136
|
+
bitrateKbps: number;
|
|
137
|
+
sampleRate: number;
|
|
138
|
+
channels: number;
|
|
139
|
+
} | null;
|
|
140
|
+
listeners: number;
|
|
141
|
+
}
|
|
142
|
+
/** What the owner sees: everything but the files' contents. */
|
|
143
|
+
export interface OwnerStation extends Omit<Station, "failures" | "idempotencyKey"> {
|
|
144
|
+
state: Observed;
|
|
145
|
+
excluded: string[];
|
|
146
|
+
listeners: number;
|
|
147
|
+
error: string;
|
|
148
|
+
}
|