nixamp 0.3.0 → 0.4.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/directory.d.ts +126 -3
- package/dist/directory.js +171 -7
- package/dist/durable.d.ts +70 -0
- package/dist/durable.js +156 -0
- package/dist/follows.d.ts +92 -0
- package/dist/follows.js +248 -0
- package/dist/notify.d.ts +83 -0
- package/dist/notify.js +126 -0
- package/dist/optin.d.ts +37 -0
- package/dist/optin.js +122 -0
- package/dist/partyline.d.ts +259 -0
- package/dist/partyline.js +616 -0
- package/dist/paywall.js +1 -1
- package/dist/playlist.js +5 -0
- package/dist/publish.d.ts +21 -0
- package/dist/publish.js +18 -2
- package/dist/server.d.ts +15 -0
- package/dist/server.js +558 -11
- package/dist/share.d.ts +10 -0
- package/dist/share.js +12 -0
- package/package.json +5 -2
- package/src/directory.ts +232 -5
- package/src/durable.ts +215 -0
- package/src/follows.ts +307 -0
- package/src/notify.ts +217 -0
- package/src/optin.ts +128 -0
- package/src/partyline.ts +742 -0
- package/src/paywall.ts +1 -1
- package/src/playlist.ts +5 -0
- package/src/publish.ts +38 -2
- package/src/server.ts +610 -10
- package/src/share.ts +13 -0
- package/web/dist/assets/{index-0wAv50Ay.css → index-DSIDSSPF.css} +1 -1
- package/web/dist/assets/index-qRguFskX.js +1 -0
- package/web/dist/index.html +27 -2
- package/web/dist/install.sh +82 -0
- package/web/dist/sw.js +45 -3
- package/web/dist/assets/index-WYJ6R4uF.js +0 -1
package/src/paywall.ts
CHANGED
|
@@ -38,7 +38,7 @@ export const DEFAULT_PAYWALL: PaywallConfig = {
|
|
|
38
38
|
};
|
|
39
39
|
|
|
40
40
|
/** Only the audio is behind the gate. */
|
|
41
|
-
export const GATED = ["/api/stream/", "/api/media/"];
|
|
41
|
+
export const GATED = ["/api/stream/", "/api/media/", "/api/live"];
|
|
42
42
|
|
|
43
43
|
export function isGated(path: string): boolean {
|
|
44
44
|
return GATED.some((prefix) => path.startsWith(prefix));
|
package/src/playlist.ts
CHANGED
|
@@ -15,6 +15,11 @@ import {
|
|
|
15
15
|
export const AUDIO_EXTENSIONS = new Set([
|
|
16
16
|
".mp3", ".flac", ".ogg", ".oga", ".opus", ".m4a", ".aac",
|
|
17
17
|
".wav", ".wma", ".aiff", ".aif", ".alac", ".mp4", ".webm",
|
|
18
|
+
// Video containers, for their audio. Everything on the way out of here is
|
|
19
|
+
// already decoded by ffmpeg and re-encoded to MP3 with -vn, so a film is a
|
|
20
|
+
// long track with a picture nobody asked for -- and a library of them was
|
|
21
|
+
// invisible to nixamp for want of the extension being on this list.
|
|
22
|
+
".mkv", ".avi", ".mov", ".m4v", ".mpg", ".mpeg", ".wmv", ".flv",
|
|
18
23
|
]);
|
|
19
24
|
|
|
20
25
|
export function isAudio(path: string): boolean {
|
package/src/publish.ts
CHANGED
|
@@ -13,13 +13,32 @@ export interface PublishTarget {
|
|
|
13
13
|
name: string;
|
|
14
14
|
/** The listen link: what a stranger opens. Never the control key. */
|
|
15
15
|
url: string;
|
|
16
|
+
/**
|
|
17
|
+
* The same stream as bytes, for a listener that cannot hold a cookie.
|
|
18
|
+
*
|
|
19
|
+
* The phone line plays this address into a call. The listen link cannot be
|
|
20
|
+
* played: it is a redirect that sets a cookie, and Telnyx fetching it once
|
|
21
|
+
* gets a 401 in JSON, which is a caller hearing nothing.
|
|
22
|
+
*/
|
|
23
|
+
audio?: string;
|
|
16
24
|
tracks: number;
|
|
17
25
|
nowPlaying: () => string;
|
|
26
|
+
/**
|
|
27
|
+
* The account this stream belongs to, from `nixamp login`.
|
|
28
|
+
*
|
|
29
|
+
* The directory used to take anybody's word for a listing. It cannot any
|
|
30
|
+
* more: a listing now carries a phone code people dial and minutes somebody
|
|
31
|
+
* pays for, so it has to be attributable. Reading the directory is still
|
|
32
|
+
* open to everyone -- it is announcing that needs a name behind it.
|
|
33
|
+
*/
|
|
34
|
+
token?: string;
|
|
18
35
|
/**
|
|
19
36
|
* Called with whatever configuration the directory sent back. This is how
|
|
20
37
|
* nixamp.com turns x402 on and off for a server without it restarting.
|
|
21
38
|
*/
|
|
22
39
|
onConfig?: (config: unknown) => void;
|
|
40
|
+
/** Called when the directory refused us for want of an account. */
|
|
41
|
+
onRefused?: () => void;
|
|
23
42
|
}
|
|
24
43
|
|
|
25
44
|
/**
|
|
@@ -45,6 +64,8 @@ export async function confirm(question: string, tty = process.stdin.isTTY === tr
|
|
|
45
64
|
export class Publisher {
|
|
46
65
|
private id: string | null = null;
|
|
47
66
|
private timer: ReturnType<typeof setInterval> | null = null;
|
|
67
|
+
/** Whether we have already said that the directory wants an account. */
|
|
68
|
+
private refused = false;
|
|
48
69
|
|
|
49
70
|
constructor(
|
|
50
71
|
private readonly target: PublishTarget,
|
|
@@ -62,16 +83,30 @@ export class Publisher {
|
|
|
62
83
|
try {
|
|
63
84
|
const response = await this.fetcher(`${this.target.directory}/api/directory`, {
|
|
64
85
|
method: "POST",
|
|
65
|
-
headers: {
|
|
86
|
+
headers: {
|
|
87
|
+
"content-type": "application/json",
|
|
88
|
+
...(this.target.token ? { authorization: `Bearer ${this.target.token}` } : {}),
|
|
89
|
+
},
|
|
66
90
|
body: JSON.stringify({
|
|
67
91
|
...(this.id ? { id: this.id } : {}),
|
|
68
92
|
name: this.target.name,
|
|
69
93
|
url: this.target.url,
|
|
94
|
+
...(this.target.audio ? { audio: this.target.audio } : {}),
|
|
70
95
|
tracks: this.target.tracks,
|
|
71
96
|
nowPlaying: this.target.nowPlaying(),
|
|
72
97
|
}),
|
|
73
98
|
});
|
|
74
|
-
if (!response.ok)
|
|
99
|
+
if (!response.ok) {
|
|
100
|
+
// Worth telling the operator about exactly once. A heartbeat that is
|
|
101
|
+
// refused every 90 seconds should not print every 90 seconds, and
|
|
102
|
+
// "could not reach the directory" would be the wrong thing to say
|
|
103
|
+
// about a directory that answered perfectly clearly.
|
|
104
|
+
if (response.status === 401 && !this.refused) {
|
|
105
|
+
this.refused = true;
|
|
106
|
+
this.target.onRefused?.();
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
75
110
|
const listing = (await response.json()) as Listing & { config?: unknown };
|
|
76
111
|
this.id = listing.id;
|
|
77
112
|
if (listing.config !== undefined) this.target.onConfig?.(listing.config);
|
|
@@ -90,6 +125,7 @@ export class Publisher {
|
|
|
90
125
|
try {
|
|
91
126
|
await this.fetcher(`${this.target.directory}/api/directory?id=${encodeURIComponent(this.id)}`, {
|
|
92
127
|
method: "DELETE",
|
|
128
|
+
...(this.target.token ? { headers: { authorization: `Bearer ${this.target.token}` } } : {}),
|
|
93
129
|
});
|
|
94
130
|
} catch {
|
|
95
131
|
// It expires on its own within the TTL, which is the point of the TTL.
|