nixamp 0.6.0 → 0.6.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/admin.d.ts CHANGED
@@ -46,6 +46,19 @@ export interface AdminOptions {
46
46
  export declare function resolveTarget(argv: string[]): AdminOptions;
47
47
  /** Seconds as something a person reads at a glance. */
48
48
  export declare function since(ms: number): string;
49
+ /**
50
+ * What a keypress contributes to a field being typed into.
51
+ *
52
+ * A paste is one event carrying the whole string, not a burst of single
53
+ * characters, so a handler that only accepted `key.length === 1` accepted
54
+ * nothing at all from a paste -- which is how you find you cannot put a URL in
55
+ * the box by any means except typing it out.
56
+ *
57
+ * The ambiguity is real and unavoidable: a pasted word of bare letters is
58
+ * indistinguishable from a key name, and loses. A URL or a path never is,
59
+ * because neither is spelled with letters alone.
60
+ */
61
+ export declare function typed(key: string): string;
49
62
  export declare function bytes(value: number): string;
50
63
  export declare function admin(argv: string[]): Promise<void>;
51
64
  export interface View {
package/dist/admin.js CHANGED
@@ -44,6 +44,31 @@ export function since(ms) {
44
44
  return `${hours}h ${minutes % 60}m`;
45
45
  return `${Math.floor(hours / 24)}d ${hours % 24}h`;
46
46
  }
47
+ /**
48
+ * A key name rather than something somebody typed: "up", "f7", "ctrl+c".
49
+ * Letters and digits only, so anything with a colon or a slash in it is text.
50
+ */
51
+ const NAMED_KEY = /^(?:[a-z]+\d*|(?:ctrl|alt|shift|meta)\+.+)$/;
52
+ /**
53
+ * What a keypress contributes to a field being typed into.
54
+ *
55
+ * A paste is one event carrying the whole string, not a burst of single
56
+ * characters, so a handler that only accepted `key.length === 1` accepted
57
+ * nothing at all from a paste -- which is how you find you cannot put a URL in
58
+ * the box by any means except typing it out.
59
+ *
60
+ * The ambiguity is real and unavoidable: a pasted word of bare letters is
61
+ * indistinguishable from a key name, and loses. A URL or a path never is,
62
+ * because neither is spelled with letters alone.
63
+ */
64
+ export function typed(key) {
65
+ if (key.length === 1)
66
+ return key >= " " && key !== "\u007f" ? key : "";
67
+ if (NAMED_KEY.test(key))
68
+ return "";
69
+ // A paste. Control characters and newlines are not part of an address.
70
+ return key.replace(/[\u0000-\u001f\u007f]/g, "");
71
+ }
47
72
  export function bytes(value) {
48
73
  const units = ["B", "KiB", "MiB", "GiB"];
49
74
  let n = value;
@@ -109,9 +134,8 @@ export async function admin(argv) {
109
134
  }
110
135
  else if (key === "backspace")
111
136
  restreaming = restreaming.slice(0, -1);
112
- // A printable key is a character; everything else is a name like "f1".
113
- else if (key.length === 1)
114
- restreaming += key;
137
+ else
138
+ restreaming += typed(key);
115
139
  app.invalidate();
116
140
  return;
117
141
  }
package/dist/server.d.ts CHANGED
@@ -239,6 +239,12 @@ export interface HandlerOptions {
239
239
  * imported so the handler stays a plain function of a request.
240
240
  */
241
241
  load: (source: string) => Promise<Track[]>;
242
+ /**
243
+ * The same source, with its tags, read without holding the event loop. Called
244
+ * after `load` and never awaited: the titles arrive into a player that is
245
+ * already playing.
246
+ */
247
+ tag?: (source: string) => Promise<Track[]>;
242
248
  /**
243
249
  * The public directory, on the instance that hosts one. Only nixamp.com
244
250
  * passes this; a nixamp on your laptop is a publisher, not a registry.
package/dist/server.js CHANGED
@@ -1623,6 +1623,15 @@ export function createHandler(engine, options) {
1623
1623
  return;
1624
1624
  }
1625
1625
  engine.replace(tracks, source);
1626
+ // Names now, tags later, here as much as at startup: re-streaming a
1627
+ // directory of five thousand files used to read every tag before it
1628
+ // answered, with the event loop held the whole time.
1629
+ if (options.tag) {
1630
+ void options
1631
+ .tag(source)
1632
+ .then((tagged) => engine.retag(tagged, source))
1633
+ .catch(() => { });
1634
+ }
1626
1635
  json(response, 200, engine.snapshot());
1627
1636
  }
1628
1637
  catch (error) {
@@ -2234,7 +2243,10 @@ export async function serve(argv, version = "0.1.0") {
2234
2243
  ffmpeg: tools.ffmpeg,
2235
2244
  ffprobe: tools.ffprobe,
2236
2245
  ...(tls ? { tls } : {}),
2237
- load: (next) => loadSource(tools, next),
2246
+ // Untagged, so a directory of five thousand files answers at once; the
2247
+ // tags follow through `tag` below.
2248
+ load: (next) => loadSource(tools, next, false),
2249
+ tag: (next) => loadTagged(tools, next),
2238
2250
  ...(directory ? { directory } : {}),
2239
2251
  ...(follows ? { follows, vapidPublicKey } : {}),
2240
2252
  ...(partyLine ? { partyLine } : {}),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "It really whips the terminal's ass. A Winamp-shaped audio player for your terminal.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/src/admin.ts CHANGED
@@ -79,6 +79,31 @@ export function since(ms: number): string {
79
79
  return `${Math.floor(hours / 24)}d ${hours % 24}h`;
80
80
  }
81
81
 
82
+ /**
83
+ * A key name rather than something somebody typed: "up", "f7", "ctrl+c".
84
+ * Letters and digits only, so anything with a colon or a slash in it is text.
85
+ */
86
+ const NAMED_KEY = /^(?:[a-z]+\d*|(?:ctrl|alt|shift|meta)\+.+)$/;
87
+
88
+ /**
89
+ * What a keypress contributes to a field being typed into.
90
+ *
91
+ * A paste is one event carrying the whole string, not a burst of single
92
+ * characters, so a handler that only accepted `key.length === 1` accepted
93
+ * nothing at all from a paste -- which is how you find you cannot put a URL in
94
+ * the box by any means except typing it out.
95
+ *
96
+ * The ambiguity is real and unavoidable: a pasted word of bare letters is
97
+ * indistinguishable from a key name, and loses. A URL or a path never is,
98
+ * because neither is spelled with letters alone.
99
+ */
100
+ export function typed(key: string): string {
101
+ if (key.length === 1) return key >= " " && key !== "\u007f" ? key : "";
102
+ if (NAMED_KEY.test(key)) return "";
103
+ // A paste. Control characters and newlines are not part of an address.
104
+ return key.replace(/[\u0000-\u001f\u007f]/g, "");
105
+ }
106
+
82
107
  export function bytes(value: number): string {
83
108
  const units = ["B", "KiB", "MiB", "GiB"];
84
109
  let n = value;
@@ -142,8 +167,7 @@ export async function admin(argv: string[]): Promise<void> {
142
167
  restreaming = "";
143
168
  if (url) void restream(target, headers, url).then(() => refresh());
144
169
  } else if (key === "backspace") restreaming = restreaming.slice(0, -1);
145
- // A printable key is a character; everything else is a name like "f1".
146
- else if (key.length === 1) restreaming += key;
170
+ else restreaming += typed(key);
147
171
  app.invalidate();
148
172
  return;
149
173
  }
package/src/server.ts CHANGED
@@ -751,6 +751,12 @@ export interface HandlerOptions {
751
751
  * imported so the handler stays a plain function of a request.
752
752
  */
753
753
  load: (source: string) => Promise<Track[]>;
754
+ /**
755
+ * The same source, with its tags, read without holding the event loop. Called
756
+ * after `load` and never awaited: the titles arrive into a player that is
757
+ * already playing.
758
+ */
759
+ tag?: (source: string) => Promise<Track[]>;
754
760
  /**
755
761
  * The public directory, on the instance that hosts one. Only nixamp.com
756
762
  * passes this; a nixamp on your laptop is a publisher, not a registry.
@@ -1916,6 +1922,15 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
1916
1922
  return;
1917
1923
  }
1918
1924
  engine.replace(tracks, source);
1925
+ // Names now, tags later, here as much as at startup: re-streaming a
1926
+ // directory of five thousand files used to read every tag before it
1927
+ // answered, with the event loop held the whole time.
1928
+ if (options.tag) {
1929
+ void options
1930
+ .tag(source)
1931
+ .then((tagged) => engine.retag(tagged, source))
1932
+ .catch(() => {});
1933
+ }
1919
1934
  json(response, 200, engine.snapshot());
1920
1935
  } catch (error) {
1921
1936
  json(response, 422, { error: (error as Error).message.replace(/^nixamp: /, "") });
@@ -2577,7 +2592,10 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
2577
2592
  ffmpeg: tools.ffmpeg,
2578
2593
  ffprobe: tools.ffprobe,
2579
2594
  ...(tls ? { tls } : {}),
2580
- load: (next) => loadSource(tools, next),
2595
+ // Untagged, so a directory of five thousand files answers at once; the
2596
+ // tags follow through `tag` below.
2597
+ load: (next) => loadSource(tools, next, false),
2598
+ tag: (next) => loadTagged(tools, next),
2581
2599
  ...(directory ? { directory } : {}),
2582
2600
  ...(follows ? { follows, vapidPublicKey } : {}),
2583
2601
  ...(partyLine ? { partyLine } : {}),
package/web/dist/sw.js CHANGED
@@ -1,5 +1,5 @@
1
1
  /* nixamp service worker — generated, do not edit */
2
- const CACHE = "nixamp-1788943887089";
2
+ const CACHE = "nixamp-1788944263913";
3
3
  const PRECACHE = [
4
4
  "/",
5
5
  "/apple-touch-icon.png",