nixamp 0.7.21 → 0.7.24

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.
@@ -28,6 +28,16 @@ export interface RemoteTrack {
28
28
  * a client puts at the top of that block so the two are not one soup.
29
29
  */
30
30
  group?: string;
31
+ /**
32
+ * Which folder it sits in, under whatever it was loaded from.
33
+ *
34
+ * Empty for a track at the top. A library is a shelf of albums and seasons,
35
+ * and five thousand files in one flat list is a list nobody can find
36
+ * anything in -- so the shape of the folders comes across and a player can
37
+ * offer them as folders. Relative, always: where the library sits on
38
+ * somebody's disk is their business.
39
+ */
40
+ folder?: string;
31
41
  }
32
42
  /** Everything a remote needs to draw the player. */
33
43
  export interface Snapshot {
package/dist/server.d.ts CHANGED
@@ -136,6 +136,8 @@ export declare function safeJoin(rootDir: string, urlPath: string): string | nul
136
136
  */
137
137
  export type Loaded = Track & {
138
138
  group?: string;
139
+ /** The folder it sits in, relative to what it was loaded from. */
140
+ folder?: string;
139
141
  /**
140
142
  * Whether this has a picture, when the name could not say.
141
143
  *
@@ -205,6 +207,17 @@ export interface Engine {
205
207
  }
206
208
  export declare function toRemoteTracks(tracks: Loaded[]): RemoteTrack[];
207
209
  export declare function hasPicture(path: string): boolean;
210
+ /**
211
+ * Where a track sits, relative to the thing it was loaded from.
212
+ *
213
+ * A library is a shelf of albums and seasons, and a flat list of five thousand
214
+ * files is one nobody can find anything in. This is what lets a player offer
215
+ * the folders as folders.
216
+ *
217
+ * Relative and never absolute: the shape of somebody's library is what a
218
+ * listener needs, and where it lives on their disk is not.
219
+ */
220
+ export declare function folderOf(path: string, from: string): string;
208
221
  /**
209
222
  * Whether the name of a source tells us anything about what is inside it.
210
223
  *
@@ -373,6 +386,18 @@ export interface HandlerOptions {
373
386
  };
374
387
  /** What this server calls itself, for the list of what is live on it. */
375
388
  serverName?: string;
389
+ /**
390
+ * The source this server was started on -- its own library.
391
+ *
392
+ * Replacing the playlist with a stream leaves no way back to it: the address
393
+ * is a path on somebody else's machine, and a person looking at a player has
394
+ * no reason to know it. Reported to an administrator so there can be a
395
+ * button rather than a thing you have to remember and retype.
396
+ *
397
+ * Admin-only, because it is a filesystem path and a viewer has no business
398
+ * with it.
399
+ */
400
+ homeSource?: string;
376
401
  /**
377
402
  * Going live: whether this server is listed, and how to change that.
378
403
  *
package/dist/server.js CHANGED
@@ -302,6 +302,7 @@ export function toRemoteTracks(tracks) {
302
302
  // Only for what was added; the library's own tracks say nothing, which is
303
303
  // how a client knows they are the library.
304
304
  ...(t.group ? { group: t.group } : {}),
305
+ ...(t.folder ? { folder: t.folder } : {}),
305
306
  }));
306
307
  }
307
308
  /** Video containers, as opposed to the songs that are most of a library. */
@@ -310,6 +311,34 @@ export function hasPicture(path) {
310
311
  const dot = path.lastIndexOf(".");
311
312
  return dot > 0 && PICTURE.has(path.slice(dot).toLowerCase());
312
313
  }
314
+ /**
315
+ * Where a track sits, relative to the thing it was loaded from.
316
+ *
317
+ * A library is a shelf of albums and seasons, and a flat list of five thousand
318
+ * files is one nobody can find anything in. This is what lets a player offer
319
+ * the folders as folders.
320
+ *
321
+ * Relative and never absolute: the shape of somebody's library is what a
322
+ * listener needs, and where it lives on their disk is not.
323
+ */
324
+ export function folderOf(path, from) {
325
+ const strip = (value) => value.replace(/\/+$/, "");
326
+ const base = strip(from);
327
+ if (base === "" || !path.startsWith(base + "/"))
328
+ return "";
329
+ const rest = path.slice(base.length + 1);
330
+ const at = rest.lastIndexOf("/");
331
+ if (at === -1)
332
+ return "";
333
+ const folder = rest.slice(0, at);
334
+ // A URL's path is percent-encoded and a person reading a folder name is not.
335
+ try {
336
+ return isRemote(path) ? decodeURIComponent(folder) : folder;
337
+ }
338
+ catch {
339
+ return folder;
340
+ }
341
+ }
313
342
  /**
314
343
  * Whether the name of a source tells us anything about what is inside it.
315
344
  *
@@ -521,7 +550,7 @@ export class PlayerEngine {
521
550
  }
522
551
  replace(tracks, root) {
523
552
  this.stop();
524
- this.tracks = tracks;
553
+ this.tracks = tracks.map((track) => ({ ...track, folder: folderOf(track.path, root) }));
525
554
  this.root = root;
526
555
  this.state.index = 0;
527
556
  this.state.position = 0;
@@ -544,7 +573,9 @@ export class PlayerEngine {
544
573
  add(tracks, from) {
545
574
  const group = sourceLabel(from);
546
575
  const known = new Set(this.tracks.map((track) => track.path));
547
- const fresh = tracks.filter((track) => !known.has(track.path)).map((track) => ({ ...track, group }));
576
+ const fresh = tracks
577
+ .filter((track) => !known.has(track.path))
578
+ .map((track) => ({ ...track, group, folder: folderOf(track.path, from) }));
548
579
  if (fresh.length === 0)
549
580
  return 0;
550
581
  this.tracks = [...this.tracks, ...fresh];
@@ -617,7 +648,7 @@ export class PlayerEngine {
617
648
  // somebody pointed the server elsewhere. Theirs wins.
618
649
  if (this.tracks.length > 0)
619
650
  return;
620
- this.tracks = tracks;
651
+ this.tracks = tracks.map((track) => ({ ...track, folder: folderOf(track.path, root) }));
621
652
  this.root = root;
622
653
  this.state.note = tracks.length === 0 ? `No audio files under ${root}.` : "";
623
654
  this.push(true);
@@ -639,7 +670,11 @@ export class PlayerEngine {
639
670
  changed = true;
640
671
  // The group is ours, not the tagger's: it knows what a track is called,
641
672
  // not which pile it is in.
642
- return { ...tagged, ...(track.group ? { group: track.group } : {}) };
673
+ return {
674
+ ...tagged,
675
+ ...(track.group ? { group: track.group } : {}),
676
+ ...(track.folder ? { folder: track.folder } : {}),
677
+ };
643
678
  });
644
679
  if (!changed)
645
680
  return;
@@ -1598,9 +1633,31 @@ export function createHandler(engine, options) {
1598
1633
  return;
1599
1634
  }
1600
1635
  if (request.method === "DELETE") {
1601
- const id = url.searchParams.get("id");
1602
- if (id)
1603
- options.directory.withdraw(id);
1636
+ const id = url.searchParams.get("id") ?? "";
1637
+ const listing = options.directory.list().find((one) => one.id === id);
1638
+ if (!listing) {
1639
+ // Already gone, or never here. Saying so plainly rather than
1640
+ // pretending to have done something.
1641
+ json(response, 404, { error: "no such listing" });
1642
+ return;
1643
+ }
1644
+ // Taking a stream out of a public directory is the owner's to do.
1645
+ //
1646
+ // This asked nobody anything: a listing id is in every copy of the
1647
+ // list, so anyone who could read the directory could empty it of other
1648
+ // people's streams. The publisher already sends its token; it was
1649
+ // simply never looked at.
1650
+ const who = options.accounts ? await options.accounts.whoIs(tokenFrom(request.headers)) : null;
1651
+ const mine = listing.ownerId !== "" && who !== null && who.id === listing.ownerId;
1652
+ if (!mine) {
1653
+ json(response, 403, {
1654
+ error: listing.ownerId === ""
1655
+ ? "that listing has no owner to prove; it leaves the list when it stops renewing"
1656
+ : "only the account that published a stream can take it off the list",
1657
+ });
1658
+ return;
1659
+ }
1660
+ options.directory.withdraw(id);
1604
1661
  json(response, 200, { ok: true });
1605
1662
  return;
1606
1663
  }
@@ -1670,6 +1727,16 @@ export function createHandler(engine, options) {
1670
1727
  listeners: one.listeners,
1671
1728
  startedAt: one.startedAt,
1672
1729
  })),
1730
+ // Anything re-streamed into this server is a live stream too, and was
1731
+ // sitting in the middle of the playlist among the files -- which is
1732
+ // what made moving between a channel and an album so confusing. Named
1733
+ // here with the first track it owns, so it can be played from the list
1734
+ // of what is live rather than hunted for among five thousand files.
1735
+ restreams: engine.groups().map((name) => ({
1736
+ name,
1737
+ at: (engine.snapshot().tracks ?? []).findIndex((track) => track.group === name),
1738
+ tracks: (engine.snapshot().tracks ?? []).filter((track) => track.group === name).length,
1739
+ })),
1673
1740
  });
1674
1741
  return;
1675
1742
  }
@@ -1916,6 +1983,10 @@ export function createHandler(engine, options) {
1916
1983
  // And which of those slots somebody is already on, because the
1917
1984
  // question you have in front of three addresses is which one is free.
1918
1985
  channels: options.channels?.list().map(({ id, name, via }) => ({ id, name, via })) ?? [],
1986
+ // What this server's own library is, and whether it is loaded, so
1987
+ // there can be a way back to it that is not retyping a path.
1988
+ home: options.homeSource ?? "",
1989
+ root: engine.snapshot(false).root,
1919
1990
  });
1920
1991
  return;
1921
1992
  }
@@ -2701,6 +2772,7 @@ export async function serve(argv, version = "0.1.0") {
2701
2772
  channels,
2702
2773
  publishUrls: () => publishUrls,
2703
2774
  serverName: options.name || hostname(),
2775
+ homeSource: root,
2704
2776
  live: {
2705
2777
  status: () => ({
2706
2778
  live: publisher !== null,
package/dist/share.d.ts CHANGED
@@ -85,21 +85,28 @@ export declare function reachableAddresses(host: string, port: number, publicUrl
85
85
  url: string;
86
86
  }[];
87
87
  /**
88
- * The two paths a key can arrive on, and what each one says about itself.
88
+ * The paths a key can arrive on, and what each one says about itself.
89
89
  *
90
90
  * `/admin/` administers and `/view/` only views. There used to be a `/s/`
91
- * that meant
92
- * neither -- just "here is a key" -- which is why somebody handed one of two
93
- * identical-looking links had no way to tell which they were holding, and
94
- * reported the controls as missing when they were never going to be there.
95
- * A link should say what it is before anybody clicks it.
91
+ * that meant neither -- just "here is a key" -- which is why somebody handed
92
+ * one of two identical-looking links had no way to tell which they were
93
+ * holding, and reported the controls as missing when they were never going to
94
+ * be there. A link should say what it is before anybody clicks it.
95
+ *
96
+ * `/a/` and `/v/` are the same two doors under shorter names they were briefly
97
+ * given. Both are read, because a link somebody was handed yesterday should
98
+ * not stop working because the spelling got clearer -- and because a player
99
+ * and the server it connects to are not upgraded on the same afternoon, which
100
+ * is exactly how a working server came to report itself as switched off.
101
+ * Links are written the long way.
96
102
  */
97
- export declare const KEY_PATHS: readonly ["/admin/", "/view/"];
103
+ export declare const KEY_PATHS: readonly ["/admin/", "/view/", "/a/", "/v/"];
98
104
  /**
99
105
  * The key in a share link, and what the link claimed to be.
100
106
  *
101
- * Both halves, because a label nobody checks is a label that can lie. `/a/`
102
- * means this administers and `/v/` means this only views; handed the other
107
+ * Both halves, because a label nobody checks is a label that can lie.
108
+ * `/admin/` means this administers and `/view/` means this only views; given
109
+ * the other
103
110
  * key, a path would otherwise have said one thing and done the other -- and
104
111
  * the dangerous direction is real: a `/view/` link built around the control key
105
112
  * reads as view-only to the person you send it to and hands them the controls.
package/dist/share.js CHANGED
@@ -242,21 +242,28 @@ export function reachableAddresses(host, port, publicUrl = "", scheme = "http")
242
242
  ];
243
243
  }
244
244
  /**
245
- * The two paths a key can arrive on, and what each one says about itself.
245
+ * The paths a key can arrive on, and what each one says about itself.
246
246
  *
247
247
  * `/admin/` administers and `/view/` only views. There used to be a `/s/`
248
- * that meant
249
- * neither -- just "here is a key" -- which is why somebody handed one of two
250
- * identical-looking links had no way to tell which they were holding, and
251
- * reported the controls as missing when they were never going to be there.
252
- * A link should say what it is before anybody clicks it.
248
+ * that meant neither -- just "here is a key" -- which is why somebody handed
249
+ * one of two identical-looking links had no way to tell which they were
250
+ * holding, and reported the controls as missing when they were never going to
251
+ * be there. A link should say what it is before anybody clicks it.
252
+ *
253
+ * `/a/` and `/v/` are the same two doors under shorter names they were briefly
254
+ * given. Both are read, because a link somebody was handed yesterday should
255
+ * not stop working because the spelling got clearer -- and because a player
256
+ * and the server it connects to are not upgraded on the same afternoon, which
257
+ * is exactly how a working server came to report itself as switched off.
258
+ * Links are written the long way.
253
259
  */
254
- export const KEY_PATHS = ["/admin/", "/view/"];
260
+ export const KEY_PATHS = ["/admin/", "/view/", "/a/", "/v/"];
255
261
  /**
256
262
  * The key in a share link, and what the link claimed to be.
257
263
  *
258
- * Both halves, because a label nobody checks is a label that can lie. `/a/`
259
- * means this administers and `/v/` means this only views; handed the other
264
+ * Both halves, because a label nobody checks is a label that can lie.
265
+ * `/admin/` means this administers and `/view/` means this only views; given
266
+ * the other
260
267
  * key, a path would otherwise have said one thing and done the other -- and
261
268
  * the dangerous direction is real: a `/view/` link built around the control key
262
269
  * reads as view-only to the person you send it to and hands them the controls.
@@ -276,7 +283,7 @@ export function keyInPath(path) {
276
283
  // A key that is not valid percent-encoding is taken as written; it will
277
284
  // fail to match anything, which is the right answer either way.
278
285
  }
279
- return { key, wants: prefix === "/admin/" ? "control" : "listen" };
286
+ return { key, wants: prefix === "/admin/" || prefix === "/a/" ? "control" : "listen" };
280
287
  }
281
288
  return null;
282
289
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.7.21",
3
+ "version": "0.7.24",
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/protocol.ts CHANGED
@@ -29,6 +29,16 @@ export interface RemoteTrack {
29
29
  * a client puts at the top of that block so the two are not one soup.
30
30
  */
31
31
  group?: string;
32
+ /**
33
+ * Which folder it sits in, under whatever it was loaded from.
34
+ *
35
+ * Empty for a track at the top. A library is a shelf of albums and seasons,
36
+ * and five thousand files in one flat list is a list nobody can find
37
+ * anything in -- so the shape of the folders comes across and a player can
38
+ * offer them as folders. Relative, always: where the library sits on
39
+ * somebody's disk is their business.
40
+ */
41
+ folder?: string;
32
42
  }
33
43
 
34
44
  /** Everything a remote needs to draw the player. */
package/src/server.ts CHANGED
@@ -408,6 +408,8 @@ export function safeJoin(rootDir: string, urlPath: string): string | null {
408
408
  */
409
409
  export type Loaded = Track & {
410
410
  group?: string;
411
+ /** The folder it sits in, relative to what it was loaded from. */
412
+ folder?: string;
411
413
  /**
412
414
  * Whether this has a picture, when the name could not say.
413
415
  *
@@ -491,6 +493,7 @@ export function toRemoteTracks(tracks: Loaded[]): RemoteTrack[] {
491
493
  // Only for what was added; the library's own tracks say nothing, which is
492
494
  // how a client knows they are the library.
493
495
  ...(t.group ? { group: t.group } : {}),
496
+ ...(t.folder ? { folder: t.folder } : {}),
494
497
  }));
495
498
  }
496
499
 
@@ -502,6 +505,32 @@ export function hasPicture(path: string): boolean {
502
505
  return dot > 0 && PICTURE.has(path.slice(dot).toLowerCase());
503
506
  }
504
507
 
508
+ /**
509
+ * Where a track sits, relative to the thing it was loaded from.
510
+ *
511
+ * A library is a shelf of albums and seasons, and a flat list of five thousand
512
+ * files is one nobody can find anything in. This is what lets a player offer
513
+ * the folders as folders.
514
+ *
515
+ * Relative and never absolute: the shape of somebody's library is what a
516
+ * listener needs, and where it lives on their disk is not.
517
+ */
518
+ export function folderOf(path: string, from: string): string {
519
+ const strip = (value: string): string => value.replace(/\/+$/, "");
520
+ const base = strip(from);
521
+ if (base === "" || !path.startsWith(base + "/")) return "";
522
+ const rest = path.slice(base.length + 1);
523
+ const at = rest.lastIndexOf("/");
524
+ if (at === -1) return "";
525
+ const folder = rest.slice(0, at);
526
+ // A URL's path is percent-encoded and a person reading a folder name is not.
527
+ try {
528
+ return isRemote(path) ? decodeURIComponent(folder) : folder;
529
+ } catch {
530
+ return folder;
531
+ }
532
+ }
533
+
505
534
  /**
506
535
  * Whether the name of a source tells us anything about what is inside it.
507
536
  *
@@ -711,7 +740,7 @@ export class PlayerEngine implements Engine {
711
740
 
712
741
  replace(tracks: Track[], root: string): void {
713
742
  this.stop();
714
- this.tracks = tracks;
743
+ this.tracks = tracks.map((track) => ({ ...track, folder: folderOf(track.path, root) }));
715
744
  this.root = root;
716
745
  this.state.index = 0;
717
746
  this.state.position = 0;
@@ -735,7 +764,9 @@ export class PlayerEngine implements Engine {
735
764
  add(tracks: Track[], from: string): number {
736
765
  const group = sourceLabel(from);
737
766
  const known = new Set(this.tracks.map((track) => track.path));
738
- const fresh = tracks.filter((track) => !known.has(track.path)).map((track) => ({ ...track, group }));
767
+ const fresh = tracks
768
+ .filter((track) => !known.has(track.path))
769
+ .map((track) => ({ ...track, group, folder: folderOf(track.path, from) }));
739
770
  if (fresh.length === 0) return 0;
740
771
  this.tracks = [...this.tracks, ...fresh];
741
772
  // The list itself changed, so it has to ride this frame; a count nobody
@@ -805,7 +836,7 @@ export class PlayerEngine implements Engine {
805
836
  // Something is already loaded, so this is a scan that finished after
806
837
  // somebody pointed the server elsewhere. Theirs wins.
807
838
  if (this.tracks.length > 0) return;
808
- this.tracks = tracks;
839
+ this.tracks = tracks.map((track) => ({ ...track, folder: folderOf(track.path, root) }));
809
840
  this.root = root;
810
841
  this.state.note = tracks.length === 0 ? `No audio files under ${root}.` : "";
811
842
  this.push(true);
@@ -827,7 +858,11 @@ export class PlayerEngine implements Engine {
827
858
  changed = true;
828
859
  // The group is ours, not the tagger's: it knows what a track is called,
829
860
  // not which pile it is in.
830
- return { ...tagged, ...(track.group ? { group: track.group } : {}) };
861
+ return {
862
+ ...tagged,
863
+ ...(track.group ? { group: track.group } : {}),
864
+ ...(track.folder ? { folder: track.folder } : {}),
865
+ };
831
866
  });
832
867
  if (!changed) return;
833
868
  this.tracks = merged;
@@ -988,6 +1023,18 @@ export interface HandlerOptions {
988
1023
  broadcast?: () => { destinations: Destination[]; settings: EncoderSettings };
989
1024
  /** What this server calls itself, for the list of what is live on it. */
990
1025
  serverName?: string;
1026
+ /**
1027
+ * The source this server was started on -- its own library.
1028
+ *
1029
+ * Replacing the playlist with a stream leaves no way back to it: the address
1030
+ * is a path on somebody else's machine, and a person looking at a player has
1031
+ * no reason to know it. Reported to an administrator so there can be a
1032
+ * button rather than a thing you have to remember and retype.
1033
+ *
1034
+ * Admin-only, because it is a filesystem path and a viewer has no business
1035
+ * with it.
1036
+ */
1037
+ homeSource?: string;
991
1038
  /**
992
1039
  * Going live: whether this server is listed, and how to change that.
993
1040
  *
@@ -1968,8 +2015,33 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
1968
2015
  return;
1969
2016
  }
1970
2017
  if (request.method === "DELETE") {
1971
- const id = url.searchParams.get("id");
1972
- if (id) options.directory.withdraw(id);
2018
+ const id = url.searchParams.get("id") ?? "";
2019
+ const listing = options.directory.list().find((one) => one.id === id);
2020
+ if (!listing) {
2021
+ // Already gone, or never here. Saying so plainly rather than
2022
+ // pretending to have done something.
2023
+ json(response, 404, { error: "no such listing" });
2024
+ return;
2025
+ }
2026
+
2027
+ // Taking a stream out of a public directory is the owner's to do.
2028
+ //
2029
+ // This asked nobody anything: a listing id is in every copy of the
2030
+ // list, so anyone who could read the directory could empty it of other
2031
+ // people's streams. The publisher already sends its token; it was
2032
+ // simply never looked at.
2033
+ const who = options.accounts ? await options.accounts.whoIs(tokenFrom(request.headers)) : null;
2034
+ const mine = listing.ownerId !== "" && who !== null && who.id === listing.ownerId;
2035
+ if (!mine) {
2036
+ json(response, 403, {
2037
+ error: listing.ownerId === ""
2038
+ ? "that listing has no owner to prove; it leaves the list when it stops renewing"
2039
+ : "only the account that published a stream can take it off the list",
2040
+ });
2041
+ return;
2042
+ }
2043
+
2044
+ options.directory.withdraw(id);
1973
2045
  json(response, 200, { ok: true });
1974
2046
  return;
1975
2047
  }
@@ -2043,6 +2115,16 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2043
2115
  listeners: one.listeners,
2044
2116
  startedAt: one.startedAt,
2045
2117
  })),
2118
+ // Anything re-streamed into this server is a live stream too, and was
2119
+ // sitting in the middle of the playlist among the files -- which is
2120
+ // what made moving between a channel and an album so confusing. Named
2121
+ // here with the first track it owns, so it can be played from the list
2122
+ // of what is live rather than hunted for among five thousand files.
2123
+ restreams: engine.groups().map((name) => ({
2124
+ name,
2125
+ at: (engine.snapshot().tracks ?? []).findIndex((track) => track.group === name),
2126
+ tracks: (engine.snapshot().tracks ?? []).filter((track) => track.group === name).length,
2127
+ })),
2046
2128
  });
2047
2129
  return;
2048
2130
  }
@@ -2305,6 +2387,10 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2305
2387
  // And which of those slots somebody is already on, because the
2306
2388
  // question you have in front of three addresses is which one is free.
2307
2389
  channels: options.channels?.list().map(({ id, name, via }) => ({ id, name, via })) ?? [],
2390
+ // What this server's own library is, and whether it is loaded, so
2391
+ // there can be a way back to it that is not retyping a path.
2392
+ home: options.homeSource ?? "",
2393
+ root: engine.snapshot(false).root,
2308
2394
  });
2309
2395
  return;
2310
2396
  }
@@ -3149,6 +3235,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
3149
3235
  channels,
3150
3236
  publishUrls: () => publishUrls,
3151
3237
  serverName: options.name || hostname(),
3238
+ homeSource: root,
3152
3239
  live: {
3153
3240
  status: () => ({
3154
3241
  live: publisher !== null,
package/src/share.ts CHANGED
@@ -272,22 +272,29 @@ export function reachableAddresses(
272
272
  }
273
273
 
274
274
  /**
275
- * The two paths a key can arrive on, and what each one says about itself.
275
+ * The paths a key can arrive on, and what each one says about itself.
276
276
  *
277
277
  * `/admin/` administers and `/view/` only views. There used to be a `/s/`
278
- * that meant
279
- * neither -- just "here is a key" -- which is why somebody handed one of two
280
- * identical-looking links had no way to tell which they were holding, and
281
- * reported the controls as missing when they were never going to be there.
282
- * A link should say what it is before anybody clicks it.
278
+ * that meant neither -- just "here is a key" -- which is why somebody handed
279
+ * one of two identical-looking links had no way to tell which they were
280
+ * holding, and reported the controls as missing when they were never going to
281
+ * be there. A link should say what it is before anybody clicks it.
282
+ *
283
+ * `/a/` and `/v/` are the same two doors under shorter names they were briefly
284
+ * given. Both are read, because a link somebody was handed yesterday should
285
+ * not stop working because the spelling got clearer -- and because a player
286
+ * and the server it connects to are not upgraded on the same afternoon, which
287
+ * is exactly how a working server came to report itself as switched off.
288
+ * Links are written the long way.
283
289
  */
284
- export const KEY_PATHS = ["/admin/", "/view/"] as const;
290
+ export const KEY_PATHS = ["/admin/", "/view/", "/a/", "/v/"] as const;
285
291
 
286
292
  /**
287
293
  * The key in a share link, and what the link claimed to be.
288
294
  *
289
- * Both halves, because a label nobody checks is a label that can lie. `/a/`
290
- * means this administers and `/v/` means this only views; handed the other
295
+ * Both halves, because a label nobody checks is a label that can lie.
296
+ * `/admin/` means this administers and `/view/` means this only views; given
297
+ * the other
291
298
  * key, a path would otherwise have said one thing and done the other -- and
292
299
  * the dangerous direction is real: a `/view/` link built around the control key
293
300
  * reads as view-only to the person you send it to and hands them the controls.
@@ -304,7 +311,7 @@ export function keyInPath(path: string): { key: string; wants: Scope } | null {
304
311
  // A key that is not valid percent-encoding is taken as written; it will
305
312
  // fail to match anything, which is the right answer either way.
306
313
  }
307
- return { key, wants: prefix === "/admin/" ? "control" : "listen" };
314
+ return { key, wants: prefix === "/admin/" || prefix === "/a/" ? "control" : "listen" };
308
315
  }
309
316
  return null;
310
317
  }
@@ -1 +1 @@
1
- import{t as e}from"./index-C_UeOo63.js";var t=3;async function n(n){let{media:r,src:i,isTv:a}=n,{default:o}=await e(async()=>{let{default:e}=await import(`./hls-n74Cnh8A.js`);return{default:e}},[]);if(!o.isSupported())return n.onError(`This browser cannot play HLS streams.`),{destroy:()=>void 0,levels:()=>[]};let s=new o({...a?{maxBufferLength:60,maxMaxBufferLength:120,backBufferLength:30,liveSyncDurationCount:4}:{backBufferLength:90},enableWorker:!0}),c=0,l=!1;s.on(o.Events.ERROR,(e,r)=>{if(!l&&r.fatal){if(c>=t){n.onError(`This stream kept failing and has been stopped.`),s.destroy();return}switch(c+=1,r.type){case o.ErrorTypes.NETWORK_ERROR:n.onNotice(`Reconnecting…`),s.startLoad();break;case o.ErrorTypes.MEDIA_ERROR:n.onNotice(`Recovering…`),s.recoverMediaError();break;default:n.onError(`This stream could not be played.`),s.destroy()}}}),s.on(o.Events.MANIFEST_PARSED,()=>{l||(n.onNotice(null),n.onReady?.({live:s.levels.length>0&&!Number.isFinite(r.duration),levels:u()}))}),s.on(o.Events.LEVEL_LOADED,(e,t)=>{l||n.onReady?.({live:t.details.live,levels:u()})}),s.on(o.Events.FRAG_BUFFERED,()=>{l||n.onNotice(null)});function u(){return s.levels.map((e,t)=>({index:t,height:e.height||null,bitrate:e.bitrate||null,label:e.height?`${String(e.height)}p`:`${String(Math.round((e.bitrate||0)/1e3))}k`}))}return s.loadSource(i),s.attachMedia(r),{destroy(){l=!0,s.destroy()},levels:u,setLevel(e){s.currentLevel=e},currentLevel:()=>s.autoLevelEnabled?-1:s.currentLevel}}export{n as createHlsEngine};
1
+ import{t as e}from"./index-CuPGZuSo.js";var t=3;async function n(n){let{media:r,src:i,isTv:a}=n,{default:o}=await e(async()=>{let{default:e}=await import(`./hls-n74Cnh8A.js`);return{default:e}},[]);if(!o.isSupported())return n.onError(`This browser cannot play HLS streams.`),{destroy:()=>void 0,levels:()=>[]};let s=new o({...a?{maxBufferLength:60,maxMaxBufferLength:120,backBufferLength:30,liveSyncDurationCount:4}:{backBufferLength:90},enableWorker:!0}),c=0,l=!1;s.on(o.Events.ERROR,(e,r)=>{if(!l&&r.fatal){if(c>=t){n.onError(`This stream kept failing and has been stopped.`),s.destroy();return}switch(c+=1,r.type){case o.ErrorTypes.NETWORK_ERROR:n.onNotice(`Reconnecting…`),s.startLoad();break;case o.ErrorTypes.MEDIA_ERROR:n.onNotice(`Recovering…`),s.recoverMediaError();break;default:n.onError(`This stream could not be played.`),s.destroy()}}}),s.on(o.Events.MANIFEST_PARSED,()=>{l||(n.onNotice(null),n.onReady?.({live:s.levels.length>0&&!Number.isFinite(r.duration),levels:u()}))}),s.on(o.Events.LEVEL_LOADED,(e,t)=>{l||n.onReady?.({live:t.details.live,levels:u()})}),s.on(o.Events.FRAG_BUFFERED,()=>{l||n.onNotice(null)});function u(){return s.levels.map((e,t)=>({index:t,height:e.height||null,bitrate:e.bitrate||null,label:e.height?`${String(e.height)}p`:`${String(Math.round((e.bitrate||0)/1e3))}k`}))}return s.loadSource(i),s.attachMedia(r),{destroy(){l=!0,s.destroy()},levels:u,setLevel(e){s.currentLevel=e},currentLevel:()=>s.autoLevelEnabled?-1:s.currentLevel}}export{n as createHlsEngine};