nixamp 0.28.1 → 0.28.3

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
@@ -574,7 +574,7 @@ uses its direct model; it does not first translate the audio into English.
574
574
 
575
575
  ### Hear it in your language
576
576
 
577
- Every signed-in account gets **five free live-use sessions per UTC day**, shared
577
+ Every signed-in account gets **ten free live-use sessions per UTC day**, shared
578
578
  across Nixamp's paid panels and upgrades. There is no timed cutoff: a session
579
579
  continues while its listener remains, within the existing API usage limits.
580
580
  Reconnects to the same session within 90 seconds reuse it. A stopped session
@@ -614,7 +614,7 @@ multiplied by five. Credentials and balances never travel in checkout URLs.
614
614
  New checkout creation is capped at five per account and fifty per account server
615
615
  per UTC day, plus IP and request throttles; retries reuse the original invoice.
616
616
 
617
- After the five free sessions, new sessions require purchased credit. Configure `COINPAY_X402_KEY`
617
+ After the ten free sessions, new sessions require purchased credit. Configure `COINPAY_X402_KEY`
618
618
  with `payments:create` permission and at least one business wallet; the scoped
619
619
  key supplies the merchant identity. Free sessions and existing credit still work during a
620
620
  checkout outage. A self-hosted operator explicitly sponsoring API usage may set
@@ -141,6 +141,8 @@ export interface Announcement {
141
141
  export interface LineupEntry {
142
142
  id: string;
143
143
  name: string;
144
+ /** Account that put this channel on the air, when known. */
145
+ startedBy?: string;
144
146
  kind: "audio" | "video";
145
147
  art: string;
146
148
  about: string;
package/dist/directory.js CHANGED
@@ -112,6 +112,9 @@ export function parseLineupEntry(input) {
112
112
  return {
113
113
  id,
114
114
  name,
115
+ ...(typeof record["startedBy"] === "string" && /^[a-zA-Z0-9_-]{1,64}$/.test(record["startedBy"])
116
+ ? { startedBy: record["startedBy"] }
117
+ : {}),
115
118
  kind: record["kind"] === "video" ? "video" : "audio",
116
119
  // A picture is somewhere a crawler can go, or nothing: a data: URL is a
117
120
  // page in a tag, and javascript: is a page in a tag that runs.
package/dist/owner.js CHANGED
@@ -132,10 +132,14 @@ export function needsAdmin(path, method = "GET") {
132
132
  export function needsMember(path, method = "GET") {
133
133
  if (method === "POST" && /^\/api\/tracks\/\d+\/live$/.test(path))
134
134
  return true;
135
+ if (method === "POST" && path === "/api/folders/live")
136
+ return true;
135
137
  if (method === "POST" && /^\/api\/catalogs\/[^/]+\/entries\/[^/]+\/live$/.test(path))
136
138
  return true;
137
139
  if (method === "POST" && /^\/api\/channels\/[^/]+\/keep$/.test(path))
138
140
  return true;
141
+ if (method === "POST" && /^\/api\/channels\/[^/]+\/(?:start|stop|restart)$/.test(path))
142
+ return true;
139
143
  // A link -- a YouTube page, an IPTV feed, a file somewhere -- is the one
140
144
  // thing a member can go live with that needs nothing on the server first,
141
145
  // and it was the one thing they could not. Going live with one is the same
package/dist/server.js CHANGED
@@ -1009,6 +1009,7 @@ export function lineupEntry(listenLink, info) {
1009
1009
  return {
1010
1010
  id: info.id,
1011
1011
  name: info.name,
1012
+ ...(info.startedBy ? { startedBy: info.startedBy } : {}),
1012
1013
  kind: info.kind ?? "audio",
1013
1014
  art: absoluteArtOf(listenLink, info),
1014
1015
  about: info.about ?? "",
@@ -3480,14 +3481,18 @@ export function createHandler(engine, options) {
3480
3481
  return;
3481
3482
  }
3482
3483
  if (!check.allowed) {
3483
- if (member !== "" && needsMember(path, request.method ?? "GET")) {
3484
+ const ownedChannel = channelId === "" ? "" : options.channels?.info(channelId)?.startedBy ?? "";
3485
+ const memberOwnsChannel = member !== "" && ownedChannel === member;
3486
+ if (member !== "" && (needsMember(path, request.method ?? "GET") || memberOwnsChannel)) {
3484
3487
  // Metered per member, before anything is done: going live is a
3485
3488
  // decoder on this machine, and a script could ask for one a
3486
3489
  // second. Three a minute is a person; more is not.
3487
- const refused = await memberThrottle.handle(requestFor(request));
3488
- if (refused) {
3489
- await answerWith(response, refused);
3490
- return;
3490
+ if (needsMember(path, request.method ?? "GET")) {
3491
+ const refused = await memberThrottle.handle(requestFor(request));
3492
+ if (refused) {
3493
+ await answerWith(response, refused);
3494
+ return;
3495
+ }
3491
3496
  }
3492
3497
  liveBy = member;
3493
3498
  }
@@ -3564,7 +3569,15 @@ export function createHandler(engine, options) {
3564
3569
  redials: one.redials ?? 0,
3565
3570
  error: one.error ?? "",
3566
3571
  // For a list: how long it is, and which entry is on, from 0.
3567
- ...(one.playlist ? { entries: one.playlist.length, entry: one.playlistAt ?? 0 } : {}),
3572
+ ...(one.playlist ? {
3573
+ entries: one.playlist.length,
3574
+ entry: one.playlistAt ?? 0,
3575
+ playlist: one.playlist.map((source) => {
3576
+ const parts = source.split(/[\\/]/).filter(Boolean);
3577
+ return parts.length > 1 ? parts.slice(-2).join(" / ") : (parts[0] || "Untitled");
3578
+ }),
3579
+ live: one.live !== false,
3580
+ } : {}),
3568
3581
  // The member who put it on, when one did: theirs to take off.
3569
3582
  startedBy: one.startedBy ?? "",
3570
3583
  // A picture of it, and a line about it, where there is one.
@@ -4027,6 +4040,62 @@ export function createHandler(engine, options) {
4027
4040
  * do and which made two files at once impossible for anybody. The owner
4028
4041
  * and any member may; the caps and the throttle above say how much.
4029
4042
  */
4043
+ if (path === "/api/folders/live" && request.method === "POST") {
4044
+ if (!options.channels) {
4045
+ json(response, 503, { error: "this server cannot carry channels" });
4046
+ return;
4047
+ }
4048
+ let body = {};
4049
+ try {
4050
+ body = JSON.parse(await readBody(request));
4051
+ }
4052
+ catch {
4053
+ json(response, 400, { error: "bad JSON" });
4054
+ return;
4055
+ }
4056
+ const indices = Array.isArray(body.indices)
4057
+ ? [...new Set(body.indices.filter((one) => Number.isInteger(one) && one >= 0))]
4058
+ : [];
4059
+ if (indices.length === 0 || indices.length > 10_000) {
4060
+ json(response, 400, { error: "choose a folder with playable files" });
4061
+ return;
4062
+ }
4063
+ const tracks = engine.snapshot().tracks ?? [];
4064
+ const selected = indices
4065
+ .map((index) => ({ index, track: tracks[index], source: engine.trackPath(index) }))
4066
+ .filter((one) => Boolean(one.track && one.source && !isRemote(one.source)));
4067
+ if (selected.length !== indices.length) {
4068
+ json(response, 404, { error: "one or more folder files are no longer available" });
4069
+ return;
4070
+ }
4071
+ selected.sort((a, b) => String(a.track.title ?? a.source).localeCompare(String(b.track.title ?? b.source), undefined, { numeric: true, sensitivity: "base" }));
4072
+ const sources = selected.map((one) => one.source);
4073
+ const channelId = cleanId(`folder-${sha("sha1").update(sources.join("\n")).digest("hex").slice(0, 12)}`);
4074
+ const name = typeof body.name === "string" && body.name.trim() !== ""
4075
+ ? body.name.trim().slice(0, 120)
4076
+ : selected[0]?.track.folder || "folder";
4077
+ if (liveBy && !options.channels.has(channelId)) {
4078
+ const refusal = memberLiveRefusal(options.channels, liveBy);
4079
+ if (refusal) {
4080
+ json(response, 429, { error: refusal });
4081
+ return;
4082
+ }
4083
+ }
4084
+ if (!options.channels.has(channelId)) {
4085
+ const started = await pullChannel(options.channels, options.ffprobe ?? ["ffprobe"], channelId, name, sources[0], [], "", { playlist: sources, live: true });
4086
+ if (!started) {
4087
+ json(response, 409, { error: "that folder is already going on the air" });
4088
+ return;
4089
+ }
4090
+ if (liveBy)
4091
+ options.channels.info(channelId).startedBy = liveBy;
4092
+ }
4093
+ options.channels.keep(channelId);
4094
+ options.rememberChannels?.(rememberedNow(options.channels));
4095
+ void options.live?.announce?.();
4096
+ json(response, 200, { channel: channelId, name, video: options.channels.kindOf(channelId) !== "audio" });
4097
+ return;
4098
+ }
4030
4099
  const trackLive = /^\/api\/tracks\/(\d+)\/live$/.exec(path);
4031
4100
  if (trackLive && request.method === "POST") {
4032
4101
  if (!options.channels) {
@@ -4406,7 +4475,7 @@ export function createHandler(engine, options) {
4406
4475
  json(response, 200, { ok: true, channel: id, name });
4407
4476
  return;
4408
4477
  }
4409
- if (action === undefined && request.method === "DELETE") {
4478
+ if ((action === undefined && request.method === "DELETE") || (action === "stop" && request.method === "POST")) {
4410
4479
  // A member takes off what they put on, and nothing else.
4411
4480
  if (liveBy && channels.info(id)?.startedBy !== liveBy) {
4412
4481
  json(response, 403, { error: "that stream is not yours to take off" });
@@ -4444,7 +4513,7 @@ export function createHandler(engine, options) {
4444
4513
  * take it off, put it back -- without having to know the source, which
4445
4514
  * a browser is never told.
4446
4515
  */
4447
- if (action === "restart") {
4516
+ if (action === "start" || action === "restart") {
4448
4517
  if (!channels.has(id)) {
4449
4518
  json(response, 404, { error: "nothing is playing on that channel" });
4450
4519
  return;
@@ -4453,6 +4522,10 @@ export function createHandler(engine, options) {
4453
4522
  json(response, 409, { error: "that channel is published into this server; restart it at the publisher" });
4454
4523
  return;
4455
4524
  }
4525
+ if (liveBy && channels.info(id)?.startedBy !== liveBy) {
4526
+ json(response, 403, { error: "that stream is not yours to restart" });
4527
+ return;
4528
+ }
4456
4529
  const restarted = channels.restart(id);
4457
4530
  json(response, restarted ? 200 : 409, { ok: restarted });
4458
4531
  return;
@@ -4472,6 +4545,10 @@ export function createHandler(engine, options) {
4472
4545
  // a member, counted and marked like anything else they put on.
4473
4546
  if (liveBy) {
4474
4547
  const info = channels.info(id);
4548
+ if (info && info.startedBy && info.startedBy !== liveBy) {
4549
+ json(response, 403, { error: "that stream is not yours to keep" });
4550
+ return;
4551
+ }
4475
4552
  if (info && !info.startedBy && !channels.isEphemeral(id)) {
4476
4553
  json(response, 403, { error: "that stream is the owner's" });
4477
4554
  return;
@@ -5,17 +5,12 @@ export declare const TRANSLATION_PLANS: readonly [{
5
5
  readonly id: "day";
6
6
  readonly name: "Day";
7
7
  readonly days: 1;
8
- readonly priceCents: 500;
9
- }, {
10
- readonly id: "week";
11
- readonly name: "Week";
12
- readonly days: 7;
13
- readonly priceCents: 2500;
8
+ readonly priceCents: 100;
14
9
  }, {
15
10
  readonly id: "month";
16
11
  readonly name: "Month";
17
12
  readonly days: 30;
18
- readonly priceCents: 10000;
13
+ readonly priceCents: 500;
19
14
  }];
20
15
  export type TranslationPlan = typeof TRANSLATION_PLANS[number];
21
16
  export type TranslationUsage = "transcription" | "voice";
@@ -5,9 +5,8 @@ import { SpeechError } from "./speech.js";
5
5
  import { UpgradeAllowances } from "./upgrade-allowance.js";
6
6
  export const TRANSLATION_MULTIPLIER = 5;
7
7
  export const TRANSLATION_PLANS = [
8
- { id: "day", name: "Day", days: 1, priceCents: 500 },
9
- { id: "week", name: "Week", days: 7, priceCents: 2500 },
10
- { id: "month", name: "Month", days: 30, priceCents: 10000 },
8
+ { id: "day", name: "Day", days: 1, priceCents: 100 },
9
+ { id: "month", name: "Month", days: 30, priceCents: 500 },
11
10
  ];
12
11
  const COINS = ["USDC_POL", "USDC_SOL", "SOL", "POL", "BTC", "ETH", "USDC_ETH", "BCH", "DOGE"];
13
12
  const ID = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i;
@@ -1,5 +1,5 @@
1
1
  import type { Queryable } from "./follows.ts";
2
- export declare const FREE_UPGRADE_SESSIONS = 5;
2
+ export declare const FREE_UPGRADE_SESSIONS = 10;
3
3
  export declare const FREE_UPGRADE_RECONNECT_SECONDS = 90;
4
4
  export interface UpgradeAllowance {
5
5
  limit: number;
@@ -2,7 +2,7 @@
2
2
  * Claim only on explicit use, never from balance polling or provider chunks. */
3
3
  import { createHash } from "node:crypto";
4
4
  import { SpeechError } from "./speech.js";
5
- export const FREE_UPGRADE_SESSIONS = 5;
5
+ export const FREE_UPGRADE_SESSIONS = 10;
6
6
  export const FREE_UPGRADE_RECONNECT_SECONDS = 90;
7
7
  const DAY = 86_400_000;
8
8
  export class UpgradeAllowances {
@@ -16,8 +16,11 @@ export class UpgradeAllowances {
16
16
  async ensure() {
17
17
  this.schema ??= this.db.query(`CREATE TABLE IF NOT EXISTS upgrade_daily_uses (
18
18
  by_account TEXT NOT NULL, day BIGINT NOT NULL,
19
- uses INTEGER NOT NULL CHECK (uses BETWEEN 1 AND 5),
20
- sessions JSONB NOT NULL, PRIMARY KEY (by_account, day))`).then(() => undefined)
19
+ uses INTEGER NOT NULL CHECK (uses >= 1),
20
+ sessions JSONB NOT NULL, PRIMARY KEY (by_account, day))`)
21
+ .then(() => this.db.query(`ALTER TABLE upgrade_daily_uses DROP CONSTRAINT IF EXISTS upgrade_daily_uses_uses_check`))
22
+ .then(() => this.db.query(`ALTER TABLE upgrade_daily_uses ADD CONSTRAINT upgrade_daily_uses_uses_check CHECK (uses BETWEEN 1 AND 10)`))
23
+ .then(() => undefined)
21
24
  .catch(error => { this.schema = null; throw error; });
22
25
  await this.schema;
23
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nixamp",
3
- "version": "0.28.1",
3
+ "version": "0.28.3",
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/directory.ts CHANGED
@@ -149,6 +149,8 @@ export interface Announcement {
149
149
  export interface LineupEntry {
150
150
  id: string;
151
151
  name: string;
152
+ /** Account that put this channel on the air, when known. */
153
+ startedBy?: string;
152
154
  kind: "audio" | "video";
153
155
  art: string;
154
156
  about: string;
@@ -247,6 +249,9 @@ export function parseLineupEntry(input: unknown): LineupEntry | null {
247
249
  return {
248
250
  id,
249
251
  name,
252
+ ...(typeof record["startedBy"] === "string" && /^[a-zA-Z0-9_-]{1,64}$/.test(record["startedBy"])
253
+ ? { startedBy: record["startedBy"] }
254
+ : {}),
250
255
  kind: record["kind"] === "video" ? "video" : "audio",
251
256
  // A picture is somewhere a crawler can go, or nothing: a data: URL is a
252
257
  // page in a tag, and javascript: is a page in a tag that runs.
package/src/owner.ts CHANGED
@@ -146,8 +146,10 @@ export function needsAdmin(path: string, method = "GET"): boolean {
146
146
  */
147
147
  export function needsMember(path: string, method = "GET"): boolean {
148
148
  if (method === "POST" && /^\/api\/tracks\/\d+\/live$/.test(path)) return true;
149
+ if (method === "POST" && path === "/api/folders/live") return true;
149
150
  if (method === "POST" && /^\/api\/catalogs\/[^/]+\/entries\/[^/]+\/live$/.test(path)) return true;
150
151
  if (method === "POST" && /^\/api\/channels\/[^/]+\/keep$/.test(path)) return true;
152
+ if (method === "POST" && /^\/api\/channels\/[^/]+\/(?:start|stop|restart)$/.test(path)) return true;
151
153
  // A link -- a YouTube page, an IPTV feed, a file somewhere -- is the one
152
154
  // thing a member can go live with that needs nothing on the server first,
153
155
  // and it was the one thing they could not. Going live with one is the same
package/src/server.ts CHANGED
@@ -1253,6 +1253,7 @@ export function lineupEntry(listenLink: string, info: ChannelInfo): LineupEntry
1253
1253
  return {
1254
1254
  id: info.id,
1255
1255
  name: info.name,
1256
+ ...(info.startedBy ? { startedBy: info.startedBy } : {}),
1256
1257
  kind: info.kind ?? "audio",
1257
1258
  art: absoluteArtOf(listenLink, info),
1258
1259
  about: info.about ?? "",
@@ -3926,14 +3927,18 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
3926
3927
  return;
3927
3928
  }
3928
3929
  if (!check.allowed) {
3929
- if (member !== "" && needsMember(path, request.method ?? "GET")) {
3930
+ const ownedChannel = channelId === "" ? "" : options.channels?.info(channelId)?.startedBy ?? "";
3931
+ const memberOwnsChannel = member !== "" && ownedChannel === member;
3932
+ if (member !== "" && (needsMember(path, request.method ?? "GET") || memberOwnsChannel)) {
3930
3933
  // Metered per member, before anything is done: going live is a
3931
3934
  // decoder on this machine, and a script could ask for one a
3932
3935
  // second. Three a minute is a person; more is not.
3933
- const refused = await memberThrottle.handle(requestFor(request));
3934
- if (refused) {
3935
- await answerWith(response, refused);
3936
- return;
3936
+ if (needsMember(path, request.method ?? "GET")) {
3937
+ const refused = await memberThrottle.handle(requestFor(request));
3938
+ if (refused) {
3939
+ await answerWith(response, refused);
3940
+ return;
3941
+ }
3937
3942
  }
3938
3943
  liveBy = member;
3939
3944
  } else {
@@ -4010,7 +4015,15 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
4010
4015
  redials: one.redials ?? 0,
4011
4016
  error: one.error ?? "",
4012
4017
  // For a list: how long it is, and which entry is on, from 0.
4013
- ...(one.playlist ? { entries: one.playlist.length, entry: one.playlistAt ?? 0 } : {}),
4018
+ ...(one.playlist ? {
4019
+ entries: one.playlist.length,
4020
+ entry: one.playlistAt ?? 0,
4021
+ playlist: one.playlist.map((source) => {
4022
+ const parts = source.split(/[\\/]/).filter(Boolean);
4023
+ return parts.length > 1 ? parts.slice(-2).join(" / ") : (parts[0] || "Untitled");
4024
+ }),
4025
+ live: one.live !== false,
4026
+ } : {}),
4014
4027
  // The member who put it on, when one did: theirs to take off.
4015
4028
  startedBy: one.startedBy ?? "",
4016
4029
  // A picture of it, and a line about it, where there is one.
@@ -4488,6 +4501,53 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
4488
4501
  * do and which made two files at once impossible for anybody. The owner
4489
4502
  * and any member may; the caps and the throttle above say how much.
4490
4503
  */
4504
+ if (path === "/api/folders/live" && request.method === "POST") {
4505
+ if (!options.channels) {
4506
+ json(response, 503, { error: "this server cannot carry channels" });
4507
+ return;
4508
+ }
4509
+ let body: { indices?: unknown; name?: unknown } = {};
4510
+ try { body = JSON.parse(await readBody(request)) as typeof body; } catch {
4511
+ json(response, 400, { error: "bad JSON" });
4512
+ return;
4513
+ }
4514
+ const indices = Array.isArray(body.indices)
4515
+ ? [...new Set(body.indices.filter((one): one is number => Number.isInteger(one) && one >= 0))]
4516
+ : [];
4517
+ if (indices.length === 0 || indices.length > 10_000) {
4518
+ json(response, 400, { error: "choose a folder with playable files" });
4519
+ return;
4520
+ }
4521
+ const tracks = engine.snapshot().tracks ?? [];
4522
+ const selected = indices
4523
+ .map((index) => ({ index, track: tracks[index], source: engine.trackPath(index) }))
4524
+ .filter((one): one is { index: number; track: (typeof tracks)[number]; source: string } => Boolean(one.track && one.source && !isRemote(one.source)));
4525
+ if (selected.length !== indices.length) {
4526
+ json(response, 404, { error: "one or more folder files are no longer available" });
4527
+ return;
4528
+ }
4529
+ selected.sort((a, b) => String(a.track.title ?? a.source).localeCompare(String(b.track.title ?? b.source), undefined, { numeric: true, sensitivity: "base" }));
4530
+ const sources = selected.map((one) => one.source);
4531
+ const channelId = cleanId(`folder-${sha("sha1").update(sources.join("\n")).digest("hex").slice(0, 12)}`);
4532
+ const name = typeof body.name === "string" && body.name.trim() !== ""
4533
+ ? body.name.trim().slice(0, 120)
4534
+ : selected[0]?.track.folder || "folder";
4535
+ if (liveBy && !options.channels.has(channelId)) {
4536
+ const refusal = memberLiveRefusal(options.channels, liveBy);
4537
+ if (refusal) { json(response, 429, { error: refusal }); return; }
4538
+ }
4539
+ if (!options.channels.has(channelId)) {
4540
+ const started = await pullChannel(options.channels, options.ffprobe ?? ["ffprobe"], channelId, name, sources[0]!, [], "", { playlist: sources, live: true });
4541
+ if (!started) { json(response, 409, { error: "that folder is already going on the air" }); return; }
4542
+ if (liveBy) options.channels.info(channelId)!.startedBy = liveBy;
4543
+ }
4544
+ options.channels.keep(channelId);
4545
+ options.rememberChannels?.(rememberedNow(options.channels));
4546
+ void options.live?.announce?.();
4547
+ json(response, 200, { channel: channelId, name, video: options.channels.kindOf(channelId) !== "audio" });
4548
+ return;
4549
+ }
4550
+
4491
4551
  const trackLive = /^\/api\/tracks\/(\d+)\/live$/.exec(path);
4492
4552
  if (trackLive && request.method === "POST") {
4493
4553
  if (!options.channels) {
@@ -4858,7 +4918,7 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
4858
4918
  return;
4859
4919
  }
4860
4920
 
4861
- if (action === undefined && request.method === "DELETE") {
4921
+ if ((action === undefined && request.method === "DELETE") || (action === "stop" && request.method === "POST")) {
4862
4922
  // A member takes off what they put on, and nothing else.
4863
4923
  if (liveBy && channels.info(id)?.startedBy !== liveBy) {
4864
4924
  json(response, 403, { error: "that stream is not yours to take off" });
@@ -4898,7 +4958,7 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
4898
4958
  * take it off, put it back -- without having to know the source, which
4899
4959
  * a browser is never told.
4900
4960
  */
4901
- if (action === "restart") {
4961
+ if (action === "start" || action === "restart") {
4902
4962
  if (!channels.has(id)) {
4903
4963
  json(response, 404, { error: "nothing is playing on that channel" });
4904
4964
  return;
@@ -4907,6 +4967,10 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
4907
4967
  json(response, 409, { error: "that channel is published into this server; restart it at the publisher" });
4908
4968
  return;
4909
4969
  }
4970
+ if (liveBy && channels.info(id)?.startedBy !== liveBy) {
4971
+ json(response, 403, { error: "that stream is not yours to restart" });
4972
+ return;
4973
+ }
4910
4974
  const restarted = channels.restart(id);
4911
4975
  json(response, restarted ? 200 : 409, { ok: restarted });
4912
4976
  return;
@@ -4927,6 +4991,10 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
4927
4991
  // a member, counted and marked like anything else they put on.
4928
4992
  if (liveBy) {
4929
4993
  const info = channels.info(id);
4994
+ if (info && info.startedBy && info.startedBy !== liveBy) {
4995
+ json(response, 403, { error: "that stream is not yours to keep" });
4996
+ return;
4997
+ }
4930
4998
  if (info && !info.startedBy && !channels.isEphemeral(id)) {
4931
4999
  json(response, 403, { error: "that stream is the owner's" });
4932
5000
  return;
@@ -7,9 +7,8 @@ import { UpgradeAllowances, type UpgradeAllowance } from "./upgrade-allowance.ts
7
7
 
8
8
  export const TRANSLATION_MULTIPLIER = 5;
9
9
  export const TRANSLATION_PLANS = [
10
- { id: "day", name: "Day", days: 1, priceCents: 500 },
11
- { id: "week", name: "Week", days: 7, priceCents: 2500 },
12
- { id: "month", name: "Month", days: 30, priceCents: 10000 },
10
+ { id: "day", name: "Day", days: 1, priceCents: 100 },
11
+ { id: "month", name: "Month", days: 30, priceCents: 500 },
13
12
  ] as const;
14
13
  export type TranslationPlan = typeof TRANSLATION_PLANS[number];
15
14
  export type TranslationUsage = "transcription" | "voice";
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
4
4
  import type { Queryable } from "./follows.ts";
5
5
  import { SpeechError } from "./speech.ts";
6
6
 
7
- export const FREE_UPGRADE_SESSIONS = 5;
7
+ export const FREE_UPGRADE_SESSIONS = 10;
8
8
  export const FREE_UPGRADE_RECONNECT_SECONDS = 90;
9
9
  const DAY = 86_400_000;
10
10
  export interface UpgradeAllowance {
@@ -18,8 +18,11 @@ export class UpgradeAllowances {
18
18
  async ensure(): Promise<void> {
19
19
  this.schema ??= this.db.query(`CREATE TABLE IF NOT EXISTS upgrade_daily_uses (
20
20
  by_account TEXT NOT NULL, day BIGINT NOT NULL,
21
- uses INTEGER NOT NULL CHECK (uses BETWEEN 1 AND 5),
22
- sessions JSONB NOT NULL, PRIMARY KEY (by_account, day))`).then(() => undefined)
21
+ uses INTEGER NOT NULL CHECK (uses >= 1),
22
+ sessions JSONB NOT NULL, PRIMARY KEY (by_account, day))`)
23
+ .then(() => this.db.query(`ALTER TABLE upgrade_daily_uses DROP CONSTRAINT IF EXISTS upgrade_daily_uses_uses_check`))
24
+ .then(() => this.db.query(`ALTER TABLE upgrade_daily_uses ADD CONSTRAINT upgrade_daily_uses_uses_check CHECK (uses BETWEEN 1 AND 10)`))
25
+ .then(() => undefined)
23
26
  .catch(error => { this.schema = null; throw error; });
24
27
  await this.schema;
25
28
  }
@@ -1 +1 @@
1
- import{t as e}from"./index-HDrZHpqb.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-B33hHbmF.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};