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/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: { "content-type": "application/json" },
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) return null;
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.