nixamp 0.24.0 → 0.24.2

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/README.md CHANGED
@@ -147,6 +147,12 @@ site. In the **Parties** panel, enter an **Invite code** and select **Join party
147
147
  to open its room. A listed party's **Join party** link opens the film on the
148
148
  site hosting it; **Open room** opens its nixamp room.
149
149
 
150
+ A live party also shows the current file's source and folder path, so a course
151
+ can read **Course › Section › Lecture**, with its position in that folder's
152
+ playlist. Numbered download filenames become readable lecture titles. Share
153
+ sheets, browser tabs and device media information follow the current lecture;
154
+ course names come from the server's metadata, never a guess from the filename.
155
+
150
156
  A watch party lives on the site that has the film. bittorrented.com has them:
151
157
  a six-character code, a host, and everybody at the same second. nixamp has
152
158
  rooms, chat, invitations, a directory, and five clients that can already open
@@ -331,8 +337,9 @@ is tap, talk, tap: the page records, brings the sound to 16 kHz mono itself,
331
337
  and sends nixamp.com a small WAV with the room's name on it; the ear posts
332
338
  the words to the room in the same request, and the line appears. Said is
333
339
  sent, unless the **Edit before sending** switch under the box is on, in
334
- which case the words wait in the box for Send. A wrong line is taken down
335
- with its ✕.
340
+ which case the words wait in the box for Send. A line, once sent, is public
341
+ record: there is no taking it down, not by its author and not by the
342
+ server's owner.
336
343
 
337
344
  **On the phone, too.** Every live room has a six-digit code on the party
338
345
  line (see below), and only when somebody is on the phone in a room, each
package/dist/server.js CHANGED
@@ -2241,14 +2241,20 @@ export function createHandler(engine, options) {
2241
2241
  * The trollbox: the chat for one live room, keyed by the server and
2242
2242
  * the channel so every viewer of a stream is in the same box whichever
2243
2243
  * page they came from. Reading is open; a line needs a nixamp.com
2244
- * account and is signed with its public handle; taking one down is the
2245
- * author's, or the listing owner's.
2244
+ * account and is signed with its public handle; and once sent it is
2245
+ * public record, so there is no DELETE here for anybody.
2246
2246
  */
2247
2247
  if ((path === "/api/v1/trollbox" || path.startsWith("/api/v1/trollbox/")) && options.trollbox && options.accounts) {
2248
2248
  const trollbox = options.trollbox;
2249
2249
  const child = path.startsWith("/api/v1/trollbox/") ? path.slice("/api/v1/trollbox/".length) : "";
2250
+ // Nothing lives under a line's id: a sent line is public record, so
2251
+ // there is no line to address, whatever the method.
2252
+ if (child !== "") {
2253
+ json(response, 404, { error: "no such endpoint" });
2254
+ return;
2255
+ }
2250
2256
  try {
2251
- if (request.method === "GET" && child === "") {
2257
+ if (request.method === "GET") {
2252
2258
  const where = roomFor(url.searchParams.get("server"), url.searchParams.get("channel"));
2253
2259
  if (!where) {
2254
2260
  json(response, 400, { error: "a room is a server address and a channel" });
@@ -2259,13 +2265,11 @@ export function createHandler(engine, options) {
2259
2265
  json(response, 200, {
2260
2266
  room: where.room,
2261
2267
  you: who ? ((await options.handles?.of(who.id)) || fallbackHandle(who.id)) : "",
2262
- messages: lines.map((one) => ({
2263
- id: one.id, handle: one.handle, body: one.body, createdAt: one.createdAt, mine: who !== null && one.authorId === who.id,
2264
- })),
2268
+ messages: lines.map((one) => ({ id: one.id, handle: one.handle, body: one.body, createdAt: one.createdAt })),
2265
2269
  });
2266
2270
  return;
2267
2271
  }
2268
- if (request.method === "POST" && child === "") {
2272
+ if (request.method === "POST") {
2269
2273
  const who = await options.accounts.whoIs(tokenFrom(request.headers));
2270
2274
  if (who === null) {
2271
2275
  json(response, 401, { error: "sign in to nixamp.com to chat" });
@@ -2287,34 +2291,10 @@ export function createHandler(engine, options) {
2287
2291
  const handle = (await options.handles?.of(who.id)) || fallbackHandle(who.id);
2288
2292
  const line = await trollbox.post(where, who.id, handle, body.body);
2289
2293
  readOnThePhone(where, who.id, line.handle, line.body);
2290
- json(response, 201, { message: { id: line.id, handle: line.handle, body: line.body, createdAt: line.createdAt, mine: true } });
2294
+ json(response, 201, { message: { id: line.id, handle: line.handle, body: line.body, createdAt: line.createdAt } });
2291
2295
  return;
2292
2296
  }
2293
- if (request.method === "DELETE" && child !== "") {
2294
- const who = await options.accounts.whoIs(tokenFrom(request.headers));
2295
- if (who === null) {
2296
- json(response, 401, { error: "sign in to nixamp.com first" });
2297
- return;
2298
- }
2299
- const where = roomFor(url.searchParams.get("server"), url.searchParams.get("channel"));
2300
- if (!where) {
2301
- json(response, 400, { error: "a room is a server address and a channel" });
2302
- return;
2303
- }
2304
- // The listing owner moderates their own server's rooms.
2305
- const moderator = (options.directory?.list() ?? []).some((listing) => {
2306
- try {
2307
- return listing.ownerId === who.id && new URL(listing.url).origin === where.server;
2308
- }
2309
- catch {
2310
- return false;
2311
- }
2312
- });
2313
- const removed = await trollbox.remove(where.room, child, who.id, moderator);
2314
- json(response, removed ? 200 : 404, removed ? { ok: true } : { error: "not your line, or already gone" });
2315
- return;
2316
- }
2317
- json(response, 405, { error: "GET, POST or DELETE" });
2297
+ json(response, 405, { error: "GET or POST" });
2318
2298
  }
2319
2299
  catch (error) {
2320
2300
  if (error instanceof TrollboxError)
@@ -2373,7 +2353,7 @@ export function createHandler(engine, options) {
2373
2353
  readOnThePhone(where, who.id, line.handle, line.body);
2374
2354
  json(response, 201, {
2375
2355
  ...said,
2376
- message: { id: line.id, handle: line.handle, body: line.body, createdAt: line.createdAt, mine: true },
2356
+ message: { id: line.id, handle: line.handle, body: line.body, createdAt: line.createdAt },
2377
2357
  });
2378
2358
  return;
2379
2359
  }
@@ -21,6 +21,14 @@ export declare const LANGUAGES: Record<string, {
21
21
  name: string;
22
22
  native: string;
23
23
  }>;
24
+ /**
25
+ * A Marian model's line, tidied. Given a short line it tends to run on in
26
+ * punctuation until the token cap ("Och sånt.............."), and to put a
27
+ * comma and a full stop where one was meant. A space before a mark, a mark
28
+ * repeated, and a run of different marks each become the one mark, the
29
+ * first of them.
30
+ */
31
+ export declare function tidyTranslation(text: string): string;
24
32
  /** The models a translation from one language to another goes through, or null when there is no way. */
25
33
  export declare function route(from: string, to: string, pairs?: ReadonlySet<string>): [string, string][] | null;
26
34
  export declare function modelFor(from: string, to: string): string;
package/dist/translate.js CHANGED
@@ -71,6 +71,21 @@ export const LANGUAGES = {
71
71
  id: { name: "Indonesian", native: "Bahasa Indonesia" },
72
72
  hi: { name: "Hindi", native: "हिन्दी" },
73
73
  };
74
+ /**
75
+ * A Marian model's line, tidied. Given a short line it tends to run on in
76
+ * punctuation until the token cap ("Och sånt.............."), and to put a
77
+ * comma and a full stop where one was meant. A space before a mark, a mark
78
+ * repeated, and a run of different marks each become the one mark, the
79
+ * first of them.
80
+ */
81
+ export function tidyTranslation(text) {
82
+ return text
83
+ .replace(/\s+([.,!?;:])/g, "$1")
84
+ .replace(/([.,!?;:])[.,;:]+/g, "$1")
85
+ .replace(/([!?])[!?]+/g, "$1")
86
+ .replace(/\s+/g, " ")
87
+ .trim();
88
+ }
74
89
  /** The models a translation from one language to another goes through, or null when there is no way. */
75
90
  export function route(from, to, pairs = PAIRS) {
76
91
  if (from === to)
@@ -226,7 +241,7 @@ export class Translator {
226
241
  }
227
242
  const out = [...spoken];
228
243
  which.forEach((i, at) => {
229
- out[i] = (current[at] ?? "").trim();
244
+ out[i] = tidyTranslation(current[at] ?? "");
230
245
  });
231
246
  return out;
232
247
  });
@@ -40,14 +40,13 @@ export declare class Trollbox {
40
40
  private readonly spoke;
41
41
  constructor(db: Queryable, now?: () => number);
42
42
  private ensure;
43
- /** The room's lines, oldest first, after a moment when given. */
43
+ /**
44
+ * The room's lines, oldest first, after a moment when given. Nothing
45
+ * writes deleted_at any more; the filter only keeps the lines taken down
46
+ * before a sent line became public record out of view.
47
+ */
44
48
  messages(room: string, after?: string, limit?: number): Promise<TrollboxMessage[]>;
45
49
  /** Whether this account may speak now, and the bookkeeping if so. */
46
50
  private allow;
47
51
  post(where: TrollboxRoom, authorId: string, handle: string, body: unknown): Promise<TrollboxMessage>;
48
- /**
49
- * A line taken down: by its author, or by a moderator (whoever owns the
50
- * server's listing). Anybody else's ask removes nothing and says so.
51
- */
52
- remove(room: string, id: string, byAccount: string, moderator: boolean): Promise<boolean>;
53
52
  }
package/dist/trollbox.js CHANGED
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.24.0",
3
+ "version": "0.24.2",
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/server.ts CHANGED
@@ -2771,14 +2771,20 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2771
2771
  * The trollbox: the chat for one live room, keyed by the server and
2772
2772
  * the channel so every viewer of a stream is in the same box whichever
2773
2773
  * page they came from. Reading is open; a line needs a nixamp.com
2774
- * account and is signed with its public handle; taking one down is the
2775
- * author's, or the listing owner's.
2774
+ * account and is signed with its public handle; and once sent it is
2775
+ * public record, so there is no DELETE here for anybody.
2776
2776
  */
2777
2777
  if ((path === "/api/v1/trollbox" || path.startsWith("/api/v1/trollbox/")) && options.trollbox && options.accounts) {
2778
2778
  const trollbox = options.trollbox;
2779
2779
  const child = path.startsWith("/api/v1/trollbox/") ? path.slice("/api/v1/trollbox/".length) : "";
2780
+ // Nothing lives under a line's id: a sent line is public record, so
2781
+ // there is no line to address, whatever the method.
2782
+ if (child !== "") {
2783
+ json(response, 404, { error: "no such endpoint" });
2784
+ return;
2785
+ }
2780
2786
  try {
2781
- if (request.method === "GET" && child === "") {
2787
+ if (request.method === "GET") {
2782
2788
  const where = roomFor(url.searchParams.get("server"), url.searchParams.get("channel"));
2783
2789
  if (!where) {
2784
2790
  json(response, 400, { error: "a room is a server address and a channel" });
@@ -2789,13 +2795,11 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2789
2795
  json(response, 200, {
2790
2796
  room: where.room,
2791
2797
  you: who ? ((await options.handles?.of(who.id)) || fallbackHandle(who.id)) : "",
2792
- messages: lines.map((one) => ({
2793
- id: one.id, handle: one.handle, body: one.body, createdAt: one.createdAt, mine: who !== null && one.authorId === who.id,
2794
- })),
2798
+ messages: lines.map((one) => ({ id: one.id, handle: one.handle, body: one.body, createdAt: one.createdAt })),
2795
2799
  });
2796
2800
  return;
2797
2801
  }
2798
- if (request.method === "POST" && child === "") {
2802
+ if (request.method === "POST") {
2799
2803
  const who = await options.accounts.whoIs(tokenFrom(request.headers));
2800
2804
  if (who === null) {
2801
2805
  json(response, 401, { error: "sign in to nixamp.com to chat" });
@@ -2816,33 +2820,10 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2816
2820
  const handle = (await options.handles?.of(who.id)) || fallbackHandle(who.id);
2817
2821
  const line = await trollbox.post(where, who.id, handle, body.body);
2818
2822
  readOnThePhone(where, who.id, line.handle, line.body);
2819
- json(response, 201, { message: { id: line.id, handle: line.handle, body: line.body, createdAt: line.createdAt, mine: true } });
2823
+ json(response, 201, { message: { id: line.id, handle: line.handle, body: line.body, createdAt: line.createdAt } });
2820
2824
  return;
2821
2825
  }
2822
- if (request.method === "DELETE" && child !== "") {
2823
- const who = await options.accounts.whoIs(tokenFrom(request.headers));
2824
- if (who === null) {
2825
- json(response, 401, { error: "sign in to nixamp.com first" });
2826
- return;
2827
- }
2828
- const where = roomFor(url.searchParams.get("server"), url.searchParams.get("channel"));
2829
- if (!where) {
2830
- json(response, 400, { error: "a room is a server address and a channel" });
2831
- return;
2832
- }
2833
- // The listing owner moderates their own server's rooms.
2834
- const moderator = (options.directory?.list() ?? []).some((listing) => {
2835
- try {
2836
- return listing.ownerId === who.id && new URL(listing.url).origin === where.server;
2837
- } catch {
2838
- return false;
2839
- }
2840
- });
2841
- const removed = await trollbox.remove(where.room, child, who.id, moderator);
2842
- json(response, removed ? 200 : 404, removed ? { ok: true } : { error: "not your line, or already gone" });
2843
- return;
2844
- }
2845
- json(response, 405, { error: "GET, POST or DELETE" });
2826
+ json(response, 405, { error: "GET or POST" });
2846
2827
  } catch (error) {
2847
2828
  if (error instanceof TrollboxError) json(response, error.status, { error: error.message });
2848
2829
  else throw error;
@@ -2899,7 +2880,7 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2899
2880
  readOnThePhone(where, who.id, line.handle, line.body);
2900
2881
  json(response, 201, {
2901
2882
  ...said,
2902
- message: { id: line.id, handle: line.handle, body: line.body, createdAt: line.createdAt, mine: true },
2883
+ message: { id: line.id, handle: line.handle, body: line.body, createdAt: line.createdAt },
2903
2884
  });
2904
2885
  return;
2905
2886
  }
package/src/translate.ts CHANGED
@@ -75,6 +75,22 @@ export const LANGUAGES: Record<string, { name: string; native: string }> = {
75
75
  hi: { name: "Hindi", native: "हिन्दी" },
76
76
  };
77
77
 
78
+ /**
79
+ * A Marian model's line, tidied. Given a short line it tends to run on in
80
+ * punctuation until the token cap ("Och sånt.............."), and to put a
81
+ * comma and a full stop where one was meant. A space before a mark, a mark
82
+ * repeated, and a run of different marks each become the one mark, the
83
+ * first of them.
84
+ */
85
+ export function tidyTranslation(text: string): string {
86
+ return text
87
+ .replace(/\s+([.,!?;:])/g, "$1")
88
+ .replace(/([.,!?;:])[.,;:]+/g, "$1")
89
+ .replace(/([!?])[!?]+/g, "$1")
90
+ .replace(/\s+/g, " ")
91
+ .trim();
92
+ }
93
+
78
94
  /** The models a translation from one language to another goes through, or null when there is no way. */
79
95
  export function route(from: string, to: string, pairs: ReadonlySet<string> = PAIRS): [string, string][] | null {
80
96
  if (from === to) return [];
@@ -262,7 +278,7 @@ export class Translator {
262
278
  }
263
279
  const out = [...spoken];
264
280
  which.forEach((i, at) => {
265
- out[i] = (current[at] ?? "").trim();
281
+ out[i] = tidyTranslation(current[at] ?? "");
266
282
  });
267
283
  return out;
268
284
  });
package/src/trollbox.ts CHANGED
Binary file
@@ -1 +1 @@
1
- import{t as e}from"./index-B0h4Nexr.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-DOOjAq1w.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};