nixamp 0.28.44 → 0.29.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/audio.js +3 -0
- package/dist/channels.d.ts +14 -0
- package/dist/channels.js +16 -3
- package/dist/decoder-policy.d.ts +20 -0
- package/dist/decoder-policy.js +34 -0
- package/dist/destinations.d.ts +5 -0
- package/dist/destinations.js +82 -0
- package/dist/library.d.ts +2 -0
- package/dist/library.js +4 -1
- package/dist/links.d.ts +4 -0
- package/dist/links.js +20 -2
- package/dist/live-interpreter.d.ts +1 -72
- package/dist/live-interpreter.js +1 -269
- package/dist/live-recovery.d.ts +1 -6
- package/dist/live-recovery.js +1 -11
- package/dist/live-voice.d.ts +6 -76
- package/dist/live-voice.js +6 -352
- package/dist/owner.js +4 -0
- package/dist/pcm-wav.d.ts +1 -2
- package/dist/pcm-wav.js +1 -27
- package/dist/playlist-input.d.ts +46 -0
- package/dist/playlist-input.js +158 -54
- package/dist/publish-choice.d.ts +21 -0
- package/dist/publish-choice.js +17 -0
- package/dist/server.js +66 -11
- package/dist/share.d.ts +1 -1
- package/dist/share.js +5 -4
- package/dist/shared-translation.d.ts +6 -53
- package/dist/shared-translation.js +7 -336
- package/dist/speaker-turns.d.ts +1 -32
- package/dist/speaker-turns.js +1 -30
- package/dist/speech.d.ts +3 -5
- package/dist/speech.js +4 -7
- package/dist/translation-passes.d.ts +5 -125
- package/dist/translation-passes.js +5 -294
- package/dist/upgrade-allowance.d.ts +1 -23
- package/dist/upgrade-allowance.js +1 -73
- package/dist/voice-profile.d.ts +1 -7
- package/dist/voice-profile.js +1 -53
- package/dist/voice-provider.d.ts +1 -9
- package/dist/voice-provider.js +1 -27
- package/package.json +2 -1
- package/src/audio.ts +3 -0
- package/src/channels.ts +26 -4
- package/src/decoder-policy.ts +33 -0
- package/src/destinations.ts +78 -0
- package/src/library.ts +6 -1
- package/src/links.ts +23 -4
- package/src/live-interpreter.ts +1 -213
- package/src/live-recovery.ts +1 -10
- package/src/live-voice.ts +6 -304
- package/src/owner.ts +3 -0
- package/src/pcm-wav.ts +1 -26
- package/src/playlist-input.ts +172 -50
- package/src/publish-choice.ts +35 -0
- package/src/server.ts +58 -11
- package/src/share.ts +5 -4
- package/src/shared-translation.ts +7 -222
- package/src/speaker-turns.ts +1 -34
- package/src/speech.ts +4 -5
- package/src/translation-passes.ts +6 -292
- package/src/upgrade-allowance.ts +1 -72
- package/src/voice-profile.ts +1 -46
- package/src/voice-provider.ts +1 -22
- package/web/dist/assets/{ar-CLNm1oA7.js → ar-D6_ZFDan.js} +1 -1
- package/web/dist/assets/{cs-Cptn8RbB.js → cs-DEPGOGEV.js} +1 -1
- package/web/dist/assets/{da-CRbNCFOu.js → da-CQw7SPC9.js} +1 -1
- package/web/dist/assets/{de-D4DnC5NQ.js → de-OWnobbah.js} +1 -1
- package/web/dist/assets/{es-DAHnD9bs.js → es-qMpyH9bM.js} +1 -1
- package/web/dist/assets/{fi-Csbl41Vz.js → fi-DpqqwJTD.js} +1 -1
- package/web/dist/assets/{fr-jZYQJed8.js → fr-BiO_ceen.js} +1 -1
- package/web/dist/assets/{hi-DjT9u0v_.js → hi-D-q5wZuY.js} +1 -1
- package/web/dist/assets/{hls-3VKVEQE3-BjAVnvGY.js → hls-3VKVEQE3-D2W4K8VR.js} +1 -1
- package/web/dist/assets/{hu-CVrU8gC4.js → hu-FIjHyhVC.js} +1 -1
- package/web/dist/assets/{id-B_gN24X5.js → id-gmB6lu56.js} +1 -1
- package/web/dist/assets/{index-BueBHHcv.css → index-Bo3utcDw.css} +1 -1
- package/web/dist/assets/index-UmGvzuun.js +22 -0
- package/web/dist/assets/{it-BUC1dMpV.js → it-Dxfo713A.js} +1 -1
- package/web/dist/assets/{mpegts-CnqmXTu2.js → mpegts-BT4XGr_E.js} +1 -1
- package/web/dist/assets/{mpegts-LO6RVLD6-B4QgG3Xb.js → mpegts-LO6RVLD6-CpZnlq0W.js} +1 -1
- package/web/dist/assets/{nl-CGi-aIrz.js → nl-mk5eN3Io.js} +1 -1
- package/web/dist/assets/{ru-YIbS63cK.js → ru-B2psU0kN.js} +1 -1
- package/web/dist/assets/{sv-CuJUpOCk.js → sv-Cg6waoNZ.js} +1 -1
- package/web/dist/assets/{uk-DLfG8YhV.js → uk-BcFkukLd.js} +1 -1
- package/web/dist/assets/{vi-nv2siY2T.js → vi-CnqIC1Sz.js} +1 -1
- package/web/dist/assets/{zh-P5x8w1OW.js → zh-CvnXIFvN.js} +1 -1
- package/web/dist/index.html +7 -12
- package/web/dist/sw.js +23 -23
- package/web/dist/assets/index-c3uxZS8_.js +0 -3
package/dist/audio.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* same bytes to the output process. Running two decoders instead would drift
|
|
7
7
|
* apart within seconds and the bars would stop matching what you hear.
|
|
8
8
|
*/
|
|
9
|
+
import { protocolArgs } from "./decoder-policy.js";
|
|
9
10
|
import { spawn, spawnSync } from "node:child_process";
|
|
10
11
|
import { readdirSync } from "node:fs";
|
|
11
12
|
import { homedir } from "node:os";
|
|
@@ -109,6 +110,7 @@ export function probe(tools, path) {
|
|
|
109
110
|
...rest,
|
|
110
111
|
"-v", "quiet", "-print_format", "json",
|
|
111
112
|
"-show_format", "-show_entries", "format_tags=title,artist,album",
|
|
113
|
+
...protocolArgs(path),
|
|
112
114
|
path,
|
|
113
115
|
], { encoding: "utf8", timeout: 20_000, maxBuffer: 4 * 1024 * 1024 });
|
|
114
116
|
if (result.error || result.status !== 0)
|
|
@@ -190,6 +192,7 @@ export class Stream {
|
|
|
190
192
|
// being on disk. Measured with that condition in place: still 13.8x.
|
|
191
193
|
"-re",
|
|
192
194
|
...(from > 0 ? ["-ss", String(from)] : []),
|
|
195
|
+
...protocolArgs(track.path),
|
|
193
196
|
"-i", track.path,
|
|
194
197
|
"-f", "f32le", "-ac", String(CHANNELS), "-ar", String(RATE), "-",
|
|
195
198
|
], { stdio: ["ignore", "pipe", "pipe"] });
|
package/dist/channels.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type PlaylistInput } from "./playlist-input.ts";
|
|
1
2
|
import type { Readable } from "node:stream";
|
|
2
3
|
/** Somewhere for a channel's audio to go. A response, in practice. */
|
|
3
4
|
export interface Listener {
|
|
@@ -105,6 +106,13 @@ export interface PullResume {
|
|
|
105
106
|
/** For a list of things rather than one: every entry, in order. */
|
|
106
107
|
playlist?: string[];
|
|
107
108
|
playlistAt?: number;
|
|
109
|
+
/**
|
|
110
|
+
* How a station plays its list: round and round, in a shuffled order, with
|
|
111
|
+
* edits landing on the next track and broken entries passed over. Only for
|
|
112
|
+
* lists of files on this machine; not remembered, because whoever owns the
|
|
113
|
+
* rotation (the station) puts it back.
|
|
114
|
+
*/
|
|
115
|
+
rotation?: Pick<PlaylistInput, "repeat" | "order" | "current" | "excluded" | "onFailure" | "passOver">;
|
|
108
116
|
}
|
|
109
117
|
/**
|
|
110
118
|
* A source read by us rather than by ffmpeg: what to tell ffmpeg it is,
|
|
@@ -220,6 +228,10 @@ export declare class Channel {
|
|
|
220
228
|
private redial;
|
|
221
229
|
/** For a channel playing a list: move to the next entry. Null when there is no list. */
|
|
222
230
|
private advance;
|
|
231
|
+
/** The list being played entry by entry, when it is one; what skip() moves on. */
|
|
232
|
+
private playlistInput;
|
|
233
|
+
/** On to the next entry of a list of files. False when this is not one. */
|
|
234
|
+
skip(): boolean;
|
|
223
235
|
private failures;
|
|
224
236
|
private timer;
|
|
225
237
|
/** Fires when a pulled source has said nothing for STALL. */
|
|
@@ -485,6 +497,8 @@ export declare class Channels {
|
|
|
485
497
|
setThrough(provider: ThroughProvider | null): void;
|
|
486
498
|
/** The kind of a channel, for a relay to say what it is carrying. */
|
|
487
499
|
kindOf(id: string): "audio" | "video" | undefined;
|
|
500
|
+
/** The next entry, now, on a channel playing a list of files. */
|
|
501
|
+
skip(id: string): boolean;
|
|
488
502
|
stop(id: string): boolean;
|
|
489
503
|
stopAll(): void;
|
|
490
504
|
}
|
package/dist/channels.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
* what live means. MP3 frames are self-describing, so a player finds the next
|
|
15
15
|
* frame boundary and carries on; there is nothing to catch up on.
|
|
16
16
|
*/
|
|
17
|
+
import { protocolArgs } from "./decoder-policy.js";
|
|
17
18
|
import { spawn } from "node:child_process";
|
|
18
19
|
import { randomBytes } from "node:crypto";
|
|
19
20
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
@@ -134,6 +135,12 @@ export class Channel {
|
|
|
134
135
|
redial = null;
|
|
135
136
|
/** For a channel playing a list: move to the next entry. Null when there is no list. */
|
|
136
137
|
advance = null;
|
|
138
|
+
/** The list being played entry by entry, when it is one; what skip() moves on. */
|
|
139
|
+
playlistInput = null;
|
|
140
|
+
/** On to the next entry of a list of files. False when this is not one. */
|
|
141
|
+
skip() {
|
|
142
|
+
return this.playlistInput?.skip() ?? false;
|
|
143
|
+
}
|
|
137
144
|
failures = 0;
|
|
138
145
|
timer = null;
|
|
139
146
|
/** Fires when a pulled source has said nothing for STALL. */
|
|
@@ -236,7 +243,7 @@ export class Channel {
|
|
|
236
243
|
// A list plays entry by entry: the one that is on is what gets dialled,
|
|
237
244
|
// and an entry that ended is followed by the next, at once.
|
|
238
245
|
const list = resume.playlist && resume.playlist.length > 0 ? resume.playlist : null;
|
|
239
|
-
const localPlaylist = list && list.length > 1 && list.every(one => !/^https?:\/\//i.test(one)) ? list : null;
|
|
246
|
+
const localPlaylist = list && (list.length > 1 || resume.rotation) && list.every(one => !/^https?:\/\//i.test(one)) ? list : null;
|
|
240
247
|
let at = 0;
|
|
241
248
|
if (list) {
|
|
242
249
|
this.info.playlist = list;
|
|
@@ -287,7 +294,9 @@ export class Channel {
|
|
|
287
294
|
ffprobe: this.options.ffprobe ?? this.options.ffmpeg.map(one => one.replace(/ffmpeg(?=(?:\.exe)?$)/, "ffprobe")),
|
|
288
295
|
files: localPlaylist, at: this.info.playlistAt ?? 0, position: from,
|
|
289
296
|
video: this.info.kind === "video", width: this.info.codecs?.width, height: this.info.codecs?.height,
|
|
297
|
+
...resume.rotation,
|
|
290
298
|
}) : null;
|
|
299
|
+
this.playlistInput = playlistInput;
|
|
291
300
|
const through = this.outroOn ? null : playlistInput
|
|
292
301
|
? { format: "mpegts", open: async (signal) => playlistInput.open(signal) }
|
|
293
302
|
: this.options.through?.(this.info, from, input, audio) ?? null;
|
|
@@ -338,10 +347,10 @@ export class Channel {
|
|
|
338
347
|
// What the source's site expects on the request: a user agent, a
|
|
339
348
|
// referer, a cookie. A link resolved by yt-dlp comes with these,
|
|
340
349
|
// and a CDN that got them from yt-dlp and not from us answers 403.
|
|
341
|
-
...input, ...seek, "-i", current,
|
|
350
|
+
...input, ...seek, ...protocolArgs(current), "-i", current,
|
|
342
351
|
// The sound, when the site keeps it apart from the picture: a
|
|
343
352
|
// second input, dialled the same way, that the encode maps in.
|
|
344
|
-
...(audio ? [...remoteArgs, ...(paced ? ["-re"] : []), ...input, ...seek, "-i", audio] : []),
|
|
353
|
+
...(audio ? [...remoteArgs, ...(paced ? ["-re"] : []), ...input, ...seek, ...protocolArgs(audio), "-i", audio] : []),
|
|
345
354
|
]),
|
|
346
355
|
...(playlistEncode ?? encode),
|
|
347
356
|
"pipe:1",
|
|
@@ -1159,6 +1168,10 @@ export class Channels {
|
|
|
1159
1168
|
kindOf(id) {
|
|
1160
1169
|
return this.open.get(id)?.info.kind;
|
|
1161
1170
|
}
|
|
1171
|
+
/** The next entry, now, on a channel playing a list of files. */
|
|
1172
|
+
skip(id) {
|
|
1173
|
+
return this.open.get(id)?.skip() ?? false;
|
|
1174
|
+
}
|
|
1162
1175
|
stop(id) {
|
|
1163
1176
|
const channel = this.open.get(id);
|
|
1164
1177
|
if (!channel)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a decoder may open while reading one source.
|
|
3
|
+
*
|
|
4
|
+
* ffmpeg follows references inside what it reads: an .m3u8 names segments, a
|
|
5
|
+
* concat list names files, a playlist on disk can name a URL. A file in
|
|
6
|
+
* somebody's library is untrusted input, and without a list of protocols it
|
|
7
|
+
* can make this machine fetch any address it likes, on the LAN included. So
|
|
8
|
+
* a file on disk may open files and pipes and nothing else, and a link may
|
|
9
|
+
* open the network protocols a stream needs and not the local file system.
|
|
10
|
+
*
|
|
11
|
+
* Sources this does not recognise (an ingest URL, a lavfi graph) are left
|
|
12
|
+
* alone rather than broken: the list is for what comes out of a library or a
|
|
13
|
+
* pasted link, which is where untrusted references come from.
|
|
14
|
+
*/
|
|
15
|
+
/** A file in a library: itself, and the pipes the pipeline is built from. */
|
|
16
|
+
export declare const LOCAL_PROTOCOLS = "file,pipe,fd";
|
|
17
|
+
/** A link: the network, as streams use it, and never `file`. */
|
|
18
|
+
export declare const REMOTE_PROTOCOLS: string;
|
|
19
|
+
/** The `-protocol_whitelist` to put before this source's `-i`, or nothing. */
|
|
20
|
+
export declare function protocolArgs(source: string): string[];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a decoder may open while reading one source.
|
|
3
|
+
*
|
|
4
|
+
* ffmpeg follows references inside what it reads: an .m3u8 names segments, a
|
|
5
|
+
* concat list names files, a playlist on disk can name a URL. A file in
|
|
6
|
+
* somebody's library is untrusted input, and without a list of protocols it
|
|
7
|
+
* can make this machine fetch any address it likes, on the LAN included. So
|
|
8
|
+
* a file on disk may open files and pipes and nothing else, and a link may
|
|
9
|
+
* open the network protocols a stream needs and not the local file system.
|
|
10
|
+
*
|
|
11
|
+
* Sources this does not recognise (an ingest URL, a lavfi graph) are left
|
|
12
|
+
* alone rather than broken: the list is for what comes out of a library or a
|
|
13
|
+
* pasted link, which is where untrusted references come from.
|
|
14
|
+
*/
|
|
15
|
+
/** A file in a library: itself, and the pipes the pipeline is built from. */
|
|
16
|
+
export const LOCAL_PROTOCOLS = "file,pipe,fd";
|
|
17
|
+
/** A link: the network, as streams use it, and never `file`. */
|
|
18
|
+
export const REMOTE_PROTOCOLS = [
|
|
19
|
+
"http", "https", "tcp", "tls", "crypto", "data", "httpproxy",
|
|
20
|
+
"rtmp", "rtmps", "rtmpt", "rtmpts", "rtmpe", "rtmpte", "ffrtmpcrypt", "ffrtmphttp",
|
|
21
|
+
].join(",");
|
|
22
|
+
/** The `-protocol_whitelist` to put before this source's `-i`, or nothing. */
|
|
23
|
+
export function protocolArgs(source) {
|
|
24
|
+
if (source === "-" || source.startsWith("pipe:"))
|
|
25
|
+
return ["-protocol_whitelist", LOCAL_PROTOCOLS];
|
|
26
|
+
if (/^(https?|rtmp[a-z]*):\/\//i.test(source))
|
|
27
|
+
return ["-protocol_whitelist", REMOTE_PROTOCOLS];
|
|
28
|
+
if (/^file:/i.test(source))
|
|
29
|
+
return ["-protocol_whitelist", LOCAL_PROTOCOLS];
|
|
30
|
+
// Any other scheme (two letters or more, so `C:\` is still a path).
|
|
31
|
+
if (/^[a-z][a-z0-9+.-]+:/i.test(source))
|
|
32
|
+
return [];
|
|
33
|
+
return ["-protocol_whitelist", LOCAL_PROTOCOLS];
|
|
34
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type Lookup = (host: string) => Promise<string[]>;
|
|
2
|
+
/** Not somewhere on the public internet: loopback, private, link-local, reserved. */
|
|
3
|
+
export declare function isPrivateAddress(address: string): boolean;
|
|
4
|
+
/** "" when this link may be fetched for a member, else why not. */
|
|
5
|
+
export declare function refusedDestination(link: string, lookup?: Lookup): Promise<string>;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whether a link somebody else pasted may be fetched from this machine.
|
|
3
|
+
*
|
|
4
|
+
* A member -- any nixamp.com account -- may put a link on the air on a server
|
|
5
|
+
* that is not theirs. Fetching it is a request from inside the owner's
|
|
6
|
+
* network, so a link to the router, the cloud metadata address or a service
|
|
7
|
+
* on localhost would be the member reaching things only the owner can. The
|
|
8
|
+
* owner may fetch what they like: it is their network. A member's link has to
|
|
9
|
+
* resolve to a public address, every address it resolves to, at every hop
|
|
10
|
+
* this code follows.
|
|
11
|
+
*
|
|
12
|
+
* What it cannot see: a redirect yt-dlp or ffmpeg follows on its own, and a
|
|
13
|
+
* name that resolves differently a second later. The decoder's protocol list
|
|
14
|
+
* (decoder-policy.ts) keeps those off the file system; keeping them off the
|
|
15
|
+
* LAN entirely needs the fetch to happen here, which is later work.
|
|
16
|
+
*/
|
|
17
|
+
import { lookup as dnsLookup } from "node:dns/promises";
|
|
18
|
+
import { isIP } from "node:net";
|
|
19
|
+
const defaultLookup = async (host) => (await dnsLookup(host, { all: true, verbatim: true })).map((one) => one.address);
|
|
20
|
+
function v4Number(address) {
|
|
21
|
+
return address.split(".").reduce((sum, part) => sum * 256 + Number(part), 0);
|
|
22
|
+
}
|
|
23
|
+
function inV4(address, base, bits) {
|
|
24
|
+
const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
|
|
25
|
+
return ((v4Number(address) & mask) >>> 0) === ((v4Number(base) & mask) >>> 0);
|
|
26
|
+
}
|
|
27
|
+
const PRIVATE_V4 = [
|
|
28
|
+
["0.0.0.0", 8], ["10.0.0.0", 8], ["100.64.0.0", 10], ["127.0.0.0", 8],
|
|
29
|
+
["169.254.0.0", 16], ["172.16.0.0", 12], ["192.0.0.0", 24], ["192.0.2.0", 24],
|
|
30
|
+
["192.168.0.0", 16], ["198.18.0.0", 15], ["198.51.100.0", 24], ["203.0.113.0", 24],
|
|
31
|
+
["224.0.0.0", 4], ["240.0.0.0", 4],
|
|
32
|
+
];
|
|
33
|
+
/** Not somewhere on the public internet: loopback, private, link-local, reserved. */
|
|
34
|
+
export function isPrivateAddress(address) {
|
|
35
|
+
const kind = isIP(address);
|
|
36
|
+
if (kind === 4)
|
|
37
|
+
return PRIVATE_V4.some(([base, bits]) => inV4(address, base, bits));
|
|
38
|
+
if (kind !== 6)
|
|
39
|
+
return true;
|
|
40
|
+
const lower = address.toLowerCase();
|
|
41
|
+
const mapped = /^::ffff:(\d+\.\d+\.\d+\.\d+)$/.exec(lower);
|
|
42
|
+
if (mapped)
|
|
43
|
+
return isPrivateAddress(mapped[1]);
|
|
44
|
+
if (lower === "::" || lower === "::1")
|
|
45
|
+
return true;
|
|
46
|
+
const first = parseInt(lower.split(":")[0] || "0", 16);
|
|
47
|
+
// fc00::/7 unique local, fe80::/10 link-local, ff00::/8 multicast.
|
|
48
|
+
if ((first & 0xfe00) === 0xfc00)
|
|
49
|
+
return true;
|
|
50
|
+
if ((first & 0xffc0) === 0xfe80)
|
|
51
|
+
return true;
|
|
52
|
+
if ((first & 0xff00) === 0xff00)
|
|
53
|
+
return true;
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
/** "" when this link may be fetched for a member, else why not. */
|
|
57
|
+
export async function refusedDestination(link, lookup = defaultLookup) {
|
|
58
|
+
let url;
|
|
59
|
+
try {
|
|
60
|
+
url = new URL(link);
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
return "that is not a link";
|
|
64
|
+
}
|
|
65
|
+
if (!/^(https?|rtmps?):$/i.test(url.protocol))
|
|
66
|
+
return "only http, https and rtmp links can be played here";
|
|
67
|
+
const host = url.hostname.replace(/^\[|\]$/g, "");
|
|
68
|
+
if (host === "" || /^localhost$/i.test(host) || /\.localhost$/i.test(host))
|
|
69
|
+
return "that link points at this machine";
|
|
70
|
+
let addresses;
|
|
71
|
+
try {
|
|
72
|
+
addresses = isIP(host) ? [host] : await lookup(host);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
return "that link's host does not resolve";
|
|
76
|
+
}
|
|
77
|
+
if (addresses.length === 0)
|
|
78
|
+
return "that link's host does not resolve";
|
|
79
|
+
if (addresses.some(isPrivateAddress))
|
|
80
|
+
return "that link points inside a private network, which only this server's owner may play";
|
|
81
|
+
return "";
|
|
82
|
+
}
|
package/dist/library.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export interface Config {
|
|
2
2
|
/** The folder the daemon serves when it is not told which. */
|
|
3
3
|
library?: string;
|
|
4
|
+
/** Whether this machine is listed in the public directory; unset until chosen. */
|
|
5
|
+
publish?: boolean;
|
|
4
6
|
}
|
|
5
7
|
export declare function configPath(): string;
|
|
6
8
|
export declare function readConfig(): Config;
|
package/dist/library.js
CHANGED
|
@@ -28,7 +28,10 @@ export function readConfig() {
|
|
|
28
28
|
if (typeof parsed !== "object" || parsed === null)
|
|
29
29
|
return {};
|
|
30
30
|
const record = parsed;
|
|
31
|
-
return {
|
|
31
|
+
return {
|
|
32
|
+
...(typeof record["library"] === "string" ? { library: record["library"] } : {}),
|
|
33
|
+
...(typeof record["publish"] === "boolean" ? { publish: record["publish"] } : {}),
|
|
34
|
+
};
|
|
32
35
|
}
|
|
33
36
|
catch {
|
|
34
37
|
return {};
|
package/dist/links.d.ts
CHANGED
|
@@ -70,6 +70,8 @@ export declare function playlistFrom(text: string, base: string): {
|
|
|
70
70
|
sources: string[];
|
|
71
71
|
image: string;
|
|
72
72
|
};
|
|
73
|
+
/** Asked of every address before it is fetched: "" to go ahead, else why not. */
|
|
74
|
+
export type Guard = (url: string) => Promise<string>;
|
|
73
75
|
/**
|
|
74
76
|
* A pasted .m3u, read and turned into a channel's worth of entries. Fetched
|
|
75
77
|
* here rather than by yt-dlp, which would take the first entry and stop, or
|
|
@@ -78,6 +80,7 @@ export declare function playlistFrom(text: string, base: string): {
|
|
|
78
80
|
export declare function resolvePlaylist(url: string, options?: {
|
|
79
81
|
timeoutMs?: number;
|
|
80
82
|
fetcher?: typeof fetch;
|
|
83
|
+
guard?: Guard;
|
|
81
84
|
}): Promise<ResolvedLink | {
|
|
82
85
|
error: string;
|
|
83
86
|
}>;
|
|
@@ -133,6 +136,7 @@ export declare function resolveLink(ytdlp: string[] | null, url: string, options
|
|
|
133
136
|
cookies?: string;
|
|
134
137
|
timeoutMs?: number;
|
|
135
138
|
format?: string;
|
|
139
|
+
guard?: Guard;
|
|
136
140
|
}): Promise<ResolvedLink | {
|
|
137
141
|
error: string;
|
|
138
142
|
}>;
|
package/dist/links.js
CHANGED
|
@@ -106,6 +106,7 @@ export function playlistFrom(text, base) {
|
|
|
106
106
|
const image = entries.map((entry) => pictureUrl(entry.logo)).find((one) => one !== "") ?? "";
|
|
107
107
|
return { hls: false, sources: entries.map((entry) => entry.source), image };
|
|
108
108
|
}
|
|
109
|
+
const PLAYLIST_MAX_REDIRECTS = 5;
|
|
109
110
|
/**
|
|
110
111
|
* A pasted .m3u, read and turned into a channel's worth of entries. Fetched
|
|
111
112
|
* here rather than by yt-dlp, which would take the first entry and stop, or
|
|
@@ -117,7 +118,24 @@ export async function resolvePlaylist(url, options = {}) {
|
|
|
117
118
|
const timer = setTimeout(() => abort.abort(), options.timeoutMs ?? PLAYLIST_FETCH_TIMEOUT_MS);
|
|
118
119
|
let text = "";
|
|
119
120
|
try {
|
|
120
|
-
|
|
121
|
+
// Followed here, one hop at a time, when there is a guard: a public
|
|
122
|
+
// address that redirects to the router is the router.
|
|
123
|
+
let at = url;
|
|
124
|
+
let answer;
|
|
125
|
+
for (let hop = 0;; hop += 1) {
|
|
126
|
+
if (options.guard) {
|
|
127
|
+
const refused = await options.guard(at);
|
|
128
|
+
if (refused)
|
|
129
|
+
return { error: refused };
|
|
130
|
+
}
|
|
131
|
+
answer = await get(at, { signal: abort.signal, headers: { "user-agent": "nixamp" }, redirect: options.guard ? "manual" : "follow" });
|
|
132
|
+
const next = answer.headers.get("location");
|
|
133
|
+
if (!options.guard || answer.status < 300 || answer.status >= 400 || !next)
|
|
134
|
+
break;
|
|
135
|
+
if (hop >= PLAYLIST_MAX_REDIRECTS)
|
|
136
|
+
return { error: "that playlist redirects too many times" };
|
|
137
|
+
at = new URL(next, at).toString();
|
|
138
|
+
}
|
|
121
139
|
if (!answer.ok)
|
|
122
140
|
return { error: `that playlist could not be fetched (HTTP ${answer.status})` };
|
|
123
141
|
text = (await answer.text()).slice(0, PLAYLIST_MAX_BYTES);
|
|
@@ -442,7 +460,7 @@ export async function resolveLink(ytdlp, url, options = {}) {
|
|
|
442
460
|
if (isDirectMedia(url))
|
|
443
461
|
return directLink(url);
|
|
444
462
|
if (isPlaylistLink(url))
|
|
445
|
-
return resolvePlaylist(url, { timeoutMs: options.timeoutMs });
|
|
463
|
+
return resolvePlaylist(url, { timeoutMs: options.timeoutMs, guard: options.guard });
|
|
446
464
|
// Without yt-dlp, a link is taken as the media it may well be: an IPTV
|
|
447
465
|
// feed has no extension and no page behind it, and ffprobe -- asked next,
|
|
448
466
|
// before anything goes on the air -- tells a stream from a web page in a
|
|
@@ -1,72 +1 @@
|
|
|
1
|
-
|
|
2
|
-
export interface Caption {
|
|
3
|
-
channel: string;
|
|
4
|
-
at: number;
|
|
5
|
-
until: number;
|
|
6
|
-
text: string;
|
|
7
|
-
language?: string;
|
|
8
|
-
original?: string;
|
|
9
|
-
sourceLanguage?: string;
|
|
10
|
-
speaker?: string;
|
|
11
|
-
voiceProfile?: "lower" | "higher" | "unknown";
|
|
12
|
-
}
|
|
13
|
-
export interface VoiceChoice {
|
|
14
|
-
id: string;
|
|
15
|
-
name: string;
|
|
16
|
-
gender: string;
|
|
17
|
-
language: string;
|
|
18
|
-
}
|
|
19
|
-
export interface AudioWindow {
|
|
20
|
-
samples: Float32Array;
|
|
21
|
-
at: number;
|
|
22
|
-
until: number;
|
|
23
|
-
freshAt: number;
|
|
24
|
-
}
|
|
25
|
-
export interface Speaker {
|
|
26
|
-
id: string;
|
|
27
|
-
profile: string;
|
|
28
|
-
voice: string;
|
|
29
|
-
}
|
|
30
|
-
/** Match provider labels across overlapping timestamps. A speaker absent from
|
|
31
|
-
* the rolling context gets a new label; pitch alone never identifies someone. */
|
|
32
|
-
export declare class SpeakerTracker {
|
|
33
|
-
private readonly random;
|
|
34
|
-
private previous;
|
|
35
|
-
private sequence;
|
|
36
|
-
readonly speakers: Map<string, Speaker>;
|
|
37
|
-
constructor(random?: () => number);
|
|
38
|
-
reset(): void;
|
|
39
|
-
reconcile(turns: SpeakerTurn[], at: number, voices: VoiceChoice[]): Map<string, Speaker>;
|
|
40
|
-
}
|
|
41
|
-
/** Listener-local interpretation for the playing media. One request in flight and only the latest pending audio window. */
|
|
42
|
-
export declare class Interpreter {
|
|
43
|
-
private readonly options;
|
|
44
|
-
readonly tracker: SpeakerTracker;
|
|
45
|
-
private generation;
|
|
46
|
-
private pending;
|
|
47
|
-
private committedUntil;
|
|
48
|
-
private running;
|
|
49
|
-
private controller;
|
|
50
|
-
private translating;
|
|
51
|
-
private translationController;
|
|
52
|
-
private recognitionFailures;
|
|
53
|
-
private translationFailures;
|
|
54
|
-
private translations;
|
|
55
|
-
constructor(options: {
|
|
56
|
-
language: () => string;
|
|
57
|
-
speakers: () => boolean;
|
|
58
|
-
voices: () => VoiceChoice[];
|
|
59
|
-
channel: () => string;
|
|
60
|
-
lines: (lines: Caption[]) => void;
|
|
61
|
-
status: (text: string) => void;
|
|
62
|
-
failed: (message: string) => void;
|
|
63
|
-
fetcher?: typeof fetch;
|
|
64
|
-
});
|
|
65
|
-
reset(): void;
|
|
66
|
-
push(window: AudioWindow): void;
|
|
67
|
-
private json;
|
|
68
|
-
private run;
|
|
69
|
-
private translate;
|
|
70
|
-
}
|
|
71
|
-
/** Preserve all words while respecting the speech API's character limit. */
|
|
72
|
-
export declare function splitCaption(line: Caption): Caption[];
|
|
1
|
+
export { Interpreter, SpeakerTracker, splitCaption, type Caption, type VoiceChoice, type AudioWindow, type Speaker } from "@profullstack/live-translate/core";
|