nixamp 0.30.1 → 0.32.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.
Files changed (69) hide show
  1. package/dist/channels.d.ts +12 -0
  2. package/dist/channels.js +8 -0
  3. package/dist/library-cli.d.ts +3 -0
  4. package/dist/library-cli.js +198 -0
  5. package/dist/library-index.d.ts +190 -0
  6. package/dist/library-index.js +643 -0
  7. package/dist/library-mcp.d.ts +230 -0
  8. package/dist/library-mcp.js +139 -0
  9. package/dist/library-routes.d.ts +34 -0
  10. package/dist/library-routes.js +258 -0
  11. package/dist/library-usage.d.ts +3 -0
  12. package/dist/library-usage.js +20 -0
  13. package/dist/library-user.d.ts +67 -0
  14. package/dist/library-user.js +204 -0
  15. package/dist/main.js +8 -0
  16. package/dist/mcp.d.ts +3 -0
  17. package/dist/mcp.js +10 -1
  18. package/dist/playlist-input.d.ts +10 -0
  19. package/dist/playlist-input.js +9 -0
  20. package/dist/server.d.ts +9 -0
  21. package/dist/server.js +83 -12
  22. package/dist/station-cli.js +4 -3
  23. package/dist/station-mcp.d.ts +311 -0
  24. package/dist/station-mcp.js +179 -0
  25. package/dist/station-page.d.ts +1 -1
  26. package/dist/station-page.js +2 -1
  27. package/dist/station-routes.js +2 -1
  28. package/dist/station-usage.d.ts +1 -1
  29. package/dist/station-usage.js +1 -1
  30. package/package.json +1 -1
  31. package/src/channels.ts +10 -0
  32. package/src/library-cli.ts +175 -0
  33. package/src/library-index.ts +709 -0
  34. package/src/library-mcp.ts +132 -0
  35. package/src/library-routes.ts +282 -0
  36. package/src/library-usage.ts +21 -0
  37. package/src/library-user.ts +218 -0
  38. package/src/main.ts +7 -0
  39. package/src/mcp.ts +12 -1
  40. package/src/playlist-input.ts +9 -0
  41. package/src/server.ts +83 -13
  42. package/src/station-cli.ts +4 -3
  43. package/src/station-mcp.ts +190 -0
  44. package/src/station-page.ts +2 -1
  45. package/src/station-routes.ts +2 -1
  46. package/src/station-usage.ts +1 -1
  47. package/web/dist/assets/{ar-CTUDxy1J.js → ar-C_4gIadE.js} +1 -1
  48. package/web/dist/assets/{cs-m4JTaO3o.js → cs-B18R5adY.js} +1 -1
  49. package/web/dist/assets/{da-DHnJElYB.js → da-B6w5URQ2.js} +1 -1
  50. package/web/dist/assets/{de-DkxPohnf.js → de-Dlhr9ikG.js} +1 -1
  51. package/web/dist/assets/{es-oY_G83d7.js → es-BIpArFOC.js} +1 -1
  52. package/web/dist/assets/{fi-fCO8XRdg.js → fi-o9W4XS3X.js} +1 -1
  53. package/web/dist/assets/{fr-CSupf-IN.js → fr-BgVwhPsP.js} +1 -1
  54. package/web/dist/assets/{hi-Dve7PJsC.js → hi-zIXqrXNC.js} +1 -1
  55. package/web/dist/assets/{hls-3VKVEQE3-D5-AkKSR.js → hls-3VKVEQE3-5pAjDDLr.js} +1 -1
  56. package/web/dist/assets/{hu-D1KYamzV.js → hu-Dpn_DIRi.js} +1 -1
  57. package/web/dist/assets/{id-Cofp_4aQ.js → id-Cu1V6gAm.js} +1 -1
  58. package/web/dist/assets/{index-7mCQZGNF.js → index-D-PZ4nGC.js} +2 -2
  59. package/web/dist/assets/{it-62iXQWQD.js → it-BNcoJEYQ.js} +1 -1
  60. package/web/dist/assets/{mpegts-BT5ESacM.js → mpegts-CabjhxTI.js} +1 -1
  61. package/web/dist/assets/{mpegts-LO6RVLD6-BgHd8PFT.js → mpegts-LO6RVLD6-CRSxiR1J.js} +1 -1
  62. package/web/dist/assets/{nl-CzRRG-CN.js → nl-gvivlDEE.js} +1 -1
  63. package/web/dist/assets/{ru-OykC1LJQ.js → ru-BxQnVrHu.js} +1 -1
  64. package/web/dist/assets/{sv-B8FXBDHm.js → sv-BP1JhAR8.js} +1 -1
  65. package/web/dist/assets/{uk-DhAJBjwH.js → uk-ChYhgzlf.js} +1 -1
  66. package/web/dist/assets/{vi-CdBSRReC.js → vi-CD7vzLwP.js} +1 -1
  67. package/web/dist/assets/{zh-wGWNyBEZ.js → zh-BmGS-RcE.js} +1 -1
  68. package/web/dist/index.html +1 -1
  69. package/web/dist/sw.js +22 -22
package/src/main.ts CHANGED
@@ -8,6 +8,7 @@ import { t as uiMessage, i18n, terminalUiLanguage, uiLanguage } from "./i18n.ts"
8
8
  * ffmpeg decodes; we read every sample on its way to the speakers and draw it.
9
9
  */
10
10
  import { USAGE as STATION_USAGE } from "./station-usage.ts";
11
+ import { LIBRARY_COMMANDS, LIBRARY_USAGE } from "./library-usage.ts";
11
12
  import { createApp, themes, type BrailleCanvas, type Container, type KeyEvent, type Theme } from "@profullstack/hqtui";
12
13
  import { fileURLToPath } from "node:url";
13
14
  import { resolve } from "node:path";
@@ -397,6 +398,7 @@ q or d leaves; neither stops anything. \`nixamp daemon stop\` is what stops it.
397
398
 
398
399
  export function helpFor(topic: string | undefined): string {
399
400
  if (topic === "station" || topic === "stations") return STATION_USAGE;
401
+ if (topic === "library") return LIBRARY_USAGE;
400
402
  return (topic ? TOPICS[topic] : undefined) ?? HELP;
401
403
  }
402
404
 
@@ -648,6 +650,11 @@ export async function main(): Promise<void> {
648
650
  process.exitCode = await syncCommand(rest);
649
651
  return;
650
652
  }
653
+ if (first === "library" && LIBRARY_COMMANDS.has(rest[0] ?? "")) {
654
+ const { libraryTool } = await import("./library-cli.ts");
655
+ process.exitCode = await libraryTool(rest);
656
+ return;
657
+ }
651
658
  if (first === "library") {
652
659
  const { libraryCommand } = await import("./library.ts");
653
660
  process.exitCode = await libraryCommand(rest);
package/src/mcp.ts CHANGED
@@ -30,6 +30,8 @@ import { describeRecord } from "./hash.ts";
30
30
  import { describeFile, keepFile, type Described } from "./media-local.ts";
31
31
  import { fileFingerprint, idFrom, languageCode, mediaOfUrl, transcriptIdOf } from "./transcripts.ts";
32
32
  import { personaLines, readPersona, readVoices, writePersona } from "./profile.ts";
33
+ import { STATION_TOOLS, callStationTool, isStationTool, type StationToolOptions } from "./station-mcp.ts";
34
+ import { LIBRARY_TOOLS, callLibraryTool, isLibraryTool } from "./library-mcp.ts";
33
35
 
34
36
  export const PROTOCOL_VERSION = "2025-06-18";
35
37
 
@@ -242,6 +244,8 @@ export const TOOLS: ToolDefinition[] = [
242
244
  required: ["server"],
243
245
  },
244
246
  },
247
+ ...STATION_TOOLS,
248
+ ...LIBRARY_TOOLS,
245
249
  ];
246
250
 
247
251
  interface TrollboxLine {
@@ -257,6 +261,8 @@ function said(line: TrollboxLine): string {
257
261
 
258
262
  export interface McpOptions {
259
263
  fetcher?: typeof fetch;
264
+ /** Where stations are asked; the tests hand in a local daemon. */
265
+ stations?: StationToolOptions;
260
266
  /** Injected by the tests; the session on disk otherwise. */
261
267
  session?: { site: string; token: string } | null;
262
268
  /** How a recording becomes a WAV; the tests hand in a fake. */
@@ -309,6 +315,11 @@ function describe(row: PartyRow): string {
309
315
  * pipe, and so the same call is reachable from anywhere else that wants it.
310
316
  */
311
317
  export async function callTool(name: string, args: Record<string, unknown>, options: McpOptions = {}): Promise<ToolResult> {
318
+ // Stations first: on this machine they need the daemon, not a sign-in.
319
+ if (isStationTool(name) || isLibraryTool(name)) {
320
+ const local = { ...(options.fetcher ? { fetcher: options.fetcher } : {}), ...(options.session !== undefined ? { session: options.session } : {}), ...(options.stations ?? {}) };
321
+ return isStationTool(name) ? callStationTool(name, args, local) : callLibraryTool(name, args, local);
322
+ }
312
323
  const session = options.session === undefined ? readSession() : options.session;
313
324
  if (!session) {
314
325
  return failed("This machine is not signed in to nixamp. Run `nixamp login`, or set NIXAMP_TOKEN.");
@@ -592,7 +603,7 @@ export async function handleMessage(message: Request, options: McpOptions = {}):
592
603
  return reply({
593
604
  protocolVersion: PROTOCOL_VERSION,
594
605
  capabilities: { tools: { listChanged: false } },
595
- serverInfo: { name: "nixamp", title: "nixamp: watch parties, rooms and transcripts", version: "2" },
606
+ serverInfo: { name: "nixamp", title: "nixamp: stations, watch parties, rooms and transcripts", version: "2" },
596
607
  instructions:
597
608
  "Watch parties on nixamp: a party lives on the site hosting the film and is bridged here as a room every nixamp client can join; codes are the ones that site shows, positions are seconds into the film. Rooms: say and read trollbox lines, hear a recording. Transcripts: a file, a link or a live is written down once by nixamp.com's own ear and kept under what it is; ask for it in another language and it is translated once and kept too.",
598
609
  });
@@ -53,6 +53,12 @@ export class PlaylistInputStream {
53
53
  private entries: { at: number; file: string; start: number; position: number }[] = [];
54
54
  error: Error | null = null;
55
55
  complete = false;
56
+ /**
57
+ * What each entry's own tags say, read when it was probed to be played --
58
+ * so a station can say what is on before the library has got round to
59
+ * reading the file, which on a big library is a while.
60
+ */
61
+ readonly tags = new Map<string, { title: string; artist: string; album: string }>();
56
62
  constructor(private readonly options: PlaylistInput) {}
57
63
 
58
64
  /** Every entry that has started, in the order it started, with where it sits in the stream. */
@@ -134,6 +140,9 @@ export class PlaylistInputStream {
134
140
  const codecs = await codecsOf({ ffmpeg: o.ffmpeg, ffprobe: o.ffprobe, play: null }, file, [], AbortSignal.any([signal, AbortSignal.timeout(10_000)]));
135
141
  if (signal.aborted) return;
136
142
  if (!codecs.audio && !codecs.video) throw new Error(`Cannot decode playlist entry ${at + 1}`);
143
+ if (codecs.tags && (codecs.tags.title || codecs.tags.artist)) {
144
+ this.tags.set(file, { title: codecs.tags.title ?? "", artist: codecs.tags.artist ?? "", album: codecs.tags.album ?? "" });
145
+ }
137
146
  const duration = (codecs.duration ?? 0) - position;
138
147
  if (duration <= 0) throw new Error(`No playable duration for playlist entry ${at + 1}`);
139
148
  this.entries.push({ at, file, start: offset, position });
package/src/server.ts CHANGED
@@ -72,6 +72,9 @@ import { PartyLine, telnyxSms } from "./partyline.ts";
72
72
  import { HlsPackagers, segmentType, withKey } from "./hls.ts";
73
73
  import { refusedDestination } from "./destinations.ts";
74
74
  import { handleStations, isStationListenPath } from "./station-routes.ts";
75
+ import { handleLibrary } from "./library-routes.ts";
76
+ import { LibraryIndex, probeItem, trackOf, type Probed } from "./library-index.ts";
77
+ import { ListenerStore } from "./library-user.ts";
75
78
  import { STATION_PAGE, STATION_PAGE_CSP, STATION_PAGE_HTML } from "./station-page.ts";
76
79
  import { StationRunner } from "./station-runner.ts";
77
80
  import { StationError, StationStore, channelOf, stationOfChannel, type Station } from "./stations.ts";
@@ -1706,6 +1709,8 @@ async function readBytes(request: IncomingMessage, limit: number): Promise<Uint8
1706
1709
  }
1707
1710
 
1708
1711
  export interface HandlerOptions {
1712
+ /** The library as an index: sources, items, search. */
1713
+ library?: { index: LibraryIndex; probe: (path: string) => Promise<Probed>; retag: (tracks: Track[]) => void; listeners?: ListenerStore };
1709
1714
  /** This server's stations and what keeps them on the air. */
1710
1715
  stations?: { store: StationStore; runner: StationRunner };
1711
1716
  web: string | null;
@@ -2708,6 +2713,30 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2708
2713
  return;
2709
2714
  }
2710
2715
 
2716
+ // Past the key gate is past it: the owner, or somebody given the listen
2717
+ // link, may read the library; only the owner may change it.
2718
+ if (options.library && (path === "/api/v1/library" || path.startsWith("/api/v1/library/"))) {
2719
+ const control = await controlOf();
2720
+ // Who this is: the owner's account, "owner" for the control key, or
2721
+ // any other signed-in nixamp.com account that has the link.
2722
+ const session = tokenFrom(request.headers) || url.searchParams.get("session") || "";
2723
+ const principal = control.account || (control.allowed ? "owner" : (session && options.owner ? (await options.owner.accountFor(session)) || null : null));
2724
+ await handleLibrary(request, response, path, url, {
2725
+ index: options.library.index,
2726
+ canRead: true,
2727
+ canWrite: control.allowed,
2728
+ account: control.account,
2729
+ principal,
2730
+ ...(options.library.listeners ? { listeners: options.library.listeners } : {}),
2731
+ probe: options.library.probe,
2732
+ retag: options.library.retag,
2733
+ sendFile,
2734
+ json,
2735
+ readBody,
2736
+ });
2737
+ return;
2738
+ }
2739
+
2711
2740
  if (options.stations && (path === "/api/v1/stations" || path.startsWith("/api/v1/stations/"))) {
2712
2741
  const control = await controlOf();
2713
2742
  const encrypted = (request.socket as { encrypted?: boolean }).encrypted === true;
@@ -2719,7 +2748,22 @@ export function createHandler(engine: Engine, options: HandlerOptions) {
2719
2748
  channels: options.channels!,
2720
2749
  canControl: control.allowed,
2721
2750
  account: control.account,
2722
- select: (body) => selectStationFiles(engine, body),
2751
+ select: (body) => {
2752
+ // A playlist of the owner's, as the station's files: its ready items, in its order.
2753
+ if (typeof (body as { playlist?: unknown }).playlist === "string" && options.library?.listeners) {
2754
+ const owner = control.account || "owner";
2755
+ let list;
2756
+ try {
2757
+ list = options.library.listeners.playlist(owner, (body as { playlist: string }).playlist);
2758
+ } catch (error) {
2759
+ throw new StationError("playlist_not_found", (error as Error).message, 404);
2760
+ }
2761
+ const files = list.items.map((id) => options.library!.index.item(id)).filter((one) => one?.state === "ready").map((one) => one!.path);
2762
+ if (files.length === 0) throw new StationError("empty_playlist", "none of that playlist's items are ready to play");
2763
+ return files;
2764
+ }
2765
+ return selectStationFiles(engine, body);
2766
+ },
2723
2767
  trackFor: (file) => trackFor(engine, file),
2724
2768
  base: `${scheme}://${request.headers.host ?? "localhost"}`,
2725
2769
  json,
@@ -6392,7 +6436,7 @@ function isFile(path: string): boolean {
6392
6436
  }
6393
6437
  }
6394
6438
 
6395
- function sendFile(request: IncomingMessage, response: ServerResponse, file: string): void {
6439
+ function sendFile(request: IncomingMessage, response: ServerResponse, file: string, cache?: string): void {
6396
6440
  let size: number;
6397
6441
  try {
6398
6442
  size = statSync(file).size;
@@ -6409,7 +6453,7 @@ function sendFile(request: IncomingMessage, response: ServerResponse, file: stri
6409
6453
  };
6410
6454
  // The shell must never be cached by a service worker's fetch fallback, but
6411
6455
  // hashed assets and audio can be.
6412
- headers["cache-control"] = type.startsWith("text/html") ? "no-cache" : "public, max-age=3600";
6456
+ headers["cache-control"] = cache ?? (type.startsWith("text/html") ? "no-cache" : "public, max-age=3600");
6413
6457
 
6414
6458
  if (range) {
6415
6459
  headers["content-range"] = `bytes ${range.start}-${range.end}/${size}`;
@@ -6550,6 +6594,23 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
6550
6594
  stationRunner?.onChannelEnd(info);
6551
6595
  },
6552
6596
  });
6597
+ // The library as an index kept beside the keys: what each file is, learned
6598
+ // once and asked about again only when it changes. The folder this server
6599
+ // was started on is its first source.
6600
+ const libraryIndex = new LibraryIndex(LibraryIndex.fileFor(stateDir(), options.port));
6601
+ const homeSource = (() => {
6602
+ try {
6603
+ return !isRemote(root) && statSync(root).isDirectory() ? libraryIndex.ensureSource(root, options.owner || "") : null;
6604
+ } catch {
6605
+ return null;
6606
+ }
6607
+ })();
6608
+ const probeFile = (path: string): Promise<Probed> => probeItem(tools, path);
6609
+ const retagPlayer = (tracks: Track[]): void => {
6610
+ if (tracks.length > 0) engine.retag(tracks, root);
6611
+ };
6612
+ const listeners = new ListenerStore(ListenerStore.fileFor(stateDir(), options.port));
6613
+ const library = { index: libraryIndex, probe: probeFile, retag: retagPlayer, listeners };
6553
6614
  const stationStore = new StationStore(StationStore.fileFor(stateDir(), options.port));
6554
6615
  stationRunner = new StationRunner({
6555
6616
  store: stationStore,
@@ -6562,7 +6623,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
6562
6623
  audioBitrate: known.audioBitrate,
6563
6624
  })),
6564
6625
  // The library as it is now, or as it was chosen while it is still being read.
6565
- roots: () => [engine.snapshot(false).root || chosen].filter((one) => one !== ""),
6626
+ roots: () => [...new Set([engine.snapshot(false).root || chosen, ...libraryIndex.roots()])].filter((one) => one !== ""),
6566
6627
  log: (line) => console.log(line),
6567
6628
  });
6568
6629
  const stations = { store: stationStore, runner: stationRunner };
@@ -7158,6 +7219,7 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
7158
7219
  owner,
7159
7220
  channels,
7160
7221
  stations,
7222
+ library,
7161
7223
  rememberChannels: remembering,
7162
7224
  catalogs,
7163
7225
  publishUrls: () => publishUrls,
@@ -7453,15 +7515,23 @@ export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
7453
7515
  }
7454
7516
  console.log(`nixamp serve — ${found.length} tracks under ${root}`);
7455
7517
  if (isRemote(root)) return;
7456
- // Handed the files we already found. Tagging used to walk the whole
7457
- // library a second time to discover the same paths, and that second walk
7458
- // was the one that ran with the port already open.
7459
- return loadTagged(tools, root, probeAsync, found.map((track) => track.path))
7460
- .then((tagged) => engine.retag(tagged, root))
7461
- .catch(() => {
7462
- // Filenames are a working player. A failure here is worth nothing
7463
- // but titles that stay as they are.
7464
- });
7518
+ if (!homeSource) {
7519
+ // A playlist file or a single file: tagged as it always was.
7520
+ return loadTagged(tools, root, probeAsync, found.map((track) => track.path))
7521
+ .then((tagged) => engine.retag(tagged, root))
7522
+ .catch(() => undefined);
7523
+ }
7524
+ // What is already known, at once: a restart has its tags before the
7525
+ // first listener arrives. Then only new and changed files are asked
7526
+ // about, and their tags reach the player a couple of hundred at a time
7527
+ // rather than all at the end.
7528
+ retagPlayer(libraryIndex.tracksFor(found.map((track) => track.path)));
7529
+ const { done } = libraryIndex.scan(homeSource.id, probeFile, {
7530
+ onProgress: (changed) => retagPlayer(changed.filter((one) => one.state === "ready").map(trackOf)),
7531
+ });
7532
+ return done.then((report) => {
7533
+ console.log(`nixamp serve — library: ${report.unchanged} unchanged, ${report.probed} read${report.failed ? `, ${report.failed} would not read` : ""}${report.missing ? `, ${report.missing} gone` : ""}${report.outside ? `, ${report.outside} links outside left out` : ""}.`);
7534
+ });
7465
7535
  })
7466
7536
  .catch((error: unknown) => {
7467
7537
  console.log(`nixamp: could not read ${root}: ${(error as Error).message}`);
@@ -116,14 +116,15 @@ export async function station(argv: string[]): Promise<number> {
116
116
  const root = readState()?.source ?? "";
117
117
  if (root && folder.startsWith(root)) folder = folder.slice(root.length);
118
118
  }
119
- if (items === undefined && folder === undefined) {
120
- console.error("nixamp station create needs --folder PATH or --items 0,1,2");
119
+ const playlist = flag(argv, "--playlist");
120
+ if (items === undefined && folder === undefined && playlist === undefined) {
121
+ console.error("nixamp station create needs --folder PATH, --playlist ID or --items 0,1,2");
121
122
  return 2;
122
123
  }
123
124
  const bitrate = flag(argv, "--bitrate");
124
125
  const answer = await call("POST", "/api/v1/stations", {
125
126
  name,
126
- ...(items !== undefined ? { items: items.split(",").map((one) => Number(one.trim())) } : { folder }),
127
+ ...(playlist !== undefined ? { playlist } : items !== undefined ? { items: items.split(",").map((one) => Number(one.trim())) } : { folder }),
127
128
  shuffle: has(argv, "--shuffle"),
128
129
  repeat: !has(argv, "--no-repeat"),
129
130
  ...(bitrate ? { bitrate: Number(bitrate) } : {}),
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Stations as MCP tools: what `nixamp station` does, for an agent.
3
+ *
4
+ * The same API and the same rules as the CLI and the page. On this machine
5
+ * the tools use the daemon's control key, which is what being on the machine
6
+ * means; for another server they send this machine's nixamp.com session,
7
+ * which the server accepts only if that account owns it. Publishing is its
8
+ * own tool and asks for the basis, exactly as for a person.
9
+ */
10
+ import { daemonUrl, readState } from "./daemon.ts";
11
+ import { readSession } from "./session.ts";
12
+ import { KEY_HEADER } from "./share.ts";
13
+
14
+ const STRING = { type: "string" } as const;
15
+ const ID = { ...STRING, description: "The station's id (10 characters), from station_list." };
16
+ const SERVER = { ...STRING, description: "A nixamp server's address; this machine's daemon when omitted." };
17
+
18
+ export const STATION_TOOLS = [
19
+ {
20
+ name: "station_list",
21
+ description: "List this server's stations: id, name, state (live, stopped, offline...), visibility and listening link.",
22
+ inputSchema: { type: "object", properties: { server: SERVER } },
23
+ },
24
+ {
25
+ name: "station_get",
26
+ description: "One station in full: files, rotation, state, any problem, and its links.",
27
+ inputSchema: { type: "object", properties: { server: SERVER, id: ID }, required: ["id"] },
28
+ },
29
+ {
30
+ name: "station_create",
31
+ description: "Make a station from a folder in the library (path relative to it, \"\" for all of it), a playlist, or track indexes. It starts private and stopped; nothing is published.",
32
+ inputSchema: {
33
+ type: "object",
34
+ properties: {
35
+ server: SERVER,
36
+ name: STRING,
37
+ folder: { ...STRING, description: "A folder under the library, e.g. \"Albums/Late Night\"." },
38
+ items: { type: "array", items: { type: "integer" }, description: "Track indexes, as /api/state lists them." },
39
+ playlist: { ...STRING, description: "A playlist id (playlist_list): the station plays its ready items in order." },
40
+ description: STRING,
41
+ shuffle: { type: "boolean" },
42
+ repeat: { type: "boolean", description: "Round and round (default) or once." },
43
+ bitrate: { type: "integer", description: "kbps: 64, 96, 128 (default), 160, 192, 256 or 320." },
44
+ idempotencyKey: { ...STRING, description: "Asking again with the same key returns the same station." },
45
+ },
46
+ required: ["name"],
47
+ },
48
+ },
49
+ {
50
+ name: "station_update",
51
+ description: "Rename, describe, change files or rotation. Edits to the files land on the next track.",
52
+ inputSchema: {
53
+ type: "object",
54
+ properties: { server: SERVER, id: ID, name: STRING, description: STRING, folder: STRING, items: { type: "array", items: { type: "integer" } }, shuffle: { type: "boolean" }, repeat: { type: "boolean" }, bitrate: { type: "integer" } },
55
+ required: ["id"],
56
+ },
57
+ },
58
+ {
59
+ name: "station_control",
60
+ description: "start, stop or skip a station, or retry the files it left out. Starting does not publish.",
61
+ inputSchema: {
62
+ type: "object",
63
+ properties: { server: SERVER, id: ID, action: { type: "string", enum: ["start", "stop", "skip", "retry"] } },
64
+ required: ["id", "action"],
65
+ },
66
+ },
67
+ {
68
+ name: "station_publish",
69
+ description: "Make a station unlisted (a link) or public (and listed in the nixamp.com directory). Public needs the basis on which it may be broadcast: owned, permission, licensed or public-domain. A declaration, not legal clearance.",
70
+ inputSchema: {
71
+ type: "object",
72
+ properties: {
73
+ server: SERVER,
74
+ id: ID,
75
+ visibility: { type: "string", enum: ["unlisted", "public"] },
76
+ listed: { type: "boolean", description: "In the directory (public only). Default true for public." },
77
+ basis: { type: "string", enum: ["owned", "permission", "licensed", "public-domain"] },
78
+ note: STRING,
79
+ },
80
+ required: ["id", "visibility"],
81
+ },
82
+ },
83
+ {
84
+ name: "station_unpublish",
85
+ description: "Take a station out of the directory, or make it private: its links stop working and listeners are disconnected.",
86
+ inputSchema: { type: "object", properties: { server: SERVER, id: ID, private: { type: "boolean" } }, required: ["id"] },
87
+ },
88
+ ];
89
+
90
+ interface Result {
91
+ content: { type: "text"; text: string }[];
92
+ isError?: boolean;
93
+ }
94
+
95
+ const say = (value: string, isError = false): Result => ({ content: [{ type: "text", text: value }], ...(isError ? { isError: true } : {}) });
96
+
97
+ export interface StationToolOptions {
98
+ fetcher?: typeof fetch;
99
+ /** Where the local daemon is and its key; the tests hand these in. */
100
+ local?: { url: string; key: string | null } | null;
101
+ session?: { site: string; token: string } | null;
102
+ }
103
+
104
+ /** Where to send a tool's request, and with which credential. */
105
+ export function connect(args: Record<string, unknown>, options: StationToolOptions): { base: string; headers: Record<string, string> } | { error: string } {
106
+ const server = typeof args["server"] === "string" ? args["server"].replace(/\/+$/, "") : "";
107
+ const headers: Record<string, string> = { "content-type": "application/json" };
108
+ if (server === "") {
109
+ const local = options.local === undefined ? (() => { const state = readState(); return state ? { url: daemonUrl(state), key: state.key } : null; })() : options.local;
110
+ if (!local) return { error: "No nixamp daemon runs on this machine. Start one (nixamp daemon start ~/Music) or pass `server`." };
111
+ if (local.key) headers[KEY_HEADER] = local.key;
112
+ return { base: local.url, headers };
113
+ }
114
+ const session = options.session === undefined ? readSession() : options.session;
115
+ if (!session) return { error: "Another server needs this machine signed in as its owner: nixamp login." };
116
+ headers["authorization"] = `Bearer ${session.token}`;
117
+ return { base: server, headers };
118
+ }
119
+
120
+ export function isStationTool(name: string): boolean {
121
+ return STATION_TOOLS.some((tool) => tool.name === name);
122
+ }
123
+
124
+ function line(station: Record<string, unknown>): string {
125
+ const links = (station["links"] ?? {}) as Record<string, string>;
126
+ const problem = station["error"] ? `; problem: ${String(station["error"])}` : "";
127
+ const link = station["visibility"] !== "private" && links["page"] ? `\n listen: ${links["page"]}` : "";
128
+ return `${String(station["name"])} [${String(station["id"])}] ${String(station["state"])}, ${String(station["visibility"])}${station["listed"] ? " and listed" : ""}${problem}${link}`;
129
+ }
130
+
131
+ export async function callStationTool(name: string, args: Record<string, unknown>, options: StationToolOptions = {}): Promise<Result> {
132
+ const send = options.fetcher ?? fetch;
133
+ const target = connect(args, options);
134
+ if ("error" in target) return say(target.error, true);
135
+ const { base, headers } = target;
136
+ const id = typeof args["id"] === "string" ? args["id"] : "";
137
+ const call = async (method: string, path: string, body?: unknown): Promise<{ ok: boolean; body: Record<string, unknown> }> => {
138
+ const response = await send(`${base}${path}`, { method, headers, ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
139
+ const parsed = (await response.json().catch(() => ({}))) as Record<string, unknown>;
140
+ return { ok: response.ok, body: parsed };
141
+ };
142
+ const refused = (body: Record<string, unknown>): Result => say(`${String(body["error"] ?? "refused")} (${String(body["code"] ?? "error")})`, true);
143
+ const pick = (keys: string[]): Record<string, unknown> => Object.fromEntries(keys.filter((key) => args[key] !== undefined).map((key) => [key, args[key]]));
144
+
145
+ try {
146
+ if (name === "station_list") {
147
+ const answer = await call("GET", "/api/v1/stations?limit=200");
148
+ if (!answer.ok) return refused(answer.body);
149
+ const all = (answer.body["stations"] as Record<string, unknown>[] | undefined) ?? [];
150
+ return say(all.length === 0 ? "No stations on this server yet." : all.map(line).join("\n"));
151
+ }
152
+ if (!/^[a-z0-9]{10}$/.test(id) && name !== "station_create") return say("Which station? Pass its id from station_list.", true);
153
+ if (name === "station_get") {
154
+ const answer = await call("GET", `/api/v1/stations/${id}`);
155
+ return answer.ok ? say(JSON.stringify(answer.body["station"], null, 2)) : refused(answer.body);
156
+ }
157
+ if (name === "station_create") {
158
+ const answer = await call("POST", "/api/v1/stations", pick(["name", "folder", "items", "playlist", "description", "shuffle", "repeat", "bitrate", "idempotencyKey"]));
159
+ return answer.ok ? say(`Made, private and stopped: ${line(answer.body["station"] as Record<string, unknown>)}`) : refused(answer.body);
160
+ }
161
+ if (name === "station_update") {
162
+ const answer = await call("PATCH", `/api/v1/stations/${id}`, pick(["name", "description", "folder", "items", "shuffle", "repeat", "bitrate"]));
163
+ return answer.ok ? say(line(answer.body["station"] as Record<string, unknown>)) : refused(answer.body);
164
+ }
165
+ if (name === "station_control") {
166
+ const action = String(args["action"] ?? "");
167
+ if (!["start", "stop", "skip", "retry"].includes(action)) return say("action is start, stop, skip or retry", true);
168
+ const answer = await call("POST", `/api/v1/stations/${id}/${action}`, {});
169
+ return answer.ok ? say(line(answer.body["station"] as Record<string, unknown>)) : refused(answer.body);
170
+ }
171
+ if (name === "station_publish") {
172
+ const visibility = args["visibility"] === "public" ? "public" : "unlisted";
173
+ const answer = await call("POST", `/api/v1/stations/${id}/access`, {
174
+ visibility,
175
+ listed: visibility === "public" && args["listed"] !== false,
176
+ ...(typeof args["basis"] === "string" ? { rights: { basis: args["basis"], note: typeof args["note"] === "string" ? args["note"] : "" } } : {}),
177
+ });
178
+ return answer.ok ? say(line(answer.body["station"] as Record<string, unknown>)) : refused(answer.body);
179
+ }
180
+ if (name === "station_unpublish") {
181
+ const answer = await call("POST", `/api/v1/stations/${id}/access`, args["private"] === true ? { visibility: "private" } : { listed: false });
182
+ if (!answer.ok) return refused(answer.body);
183
+ const dropped = Number(answer.body["dropped"] ?? 0);
184
+ return say(`${line(answer.body["station"] as Record<string, unknown>)}${dropped ? `\n${dropped} listener(s) disconnected.` : ""}`);
185
+ }
186
+ } catch (error) {
187
+ return say(`Could not reach ${base}: ${(error as Error).message}`, true);
188
+ }
189
+ return say(`No such tool: ${name}`, true);
190
+ }
@@ -101,6 +101,7 @@ const PAGE = `<!doctype html>
101
101
  <meta charset="utf-8">
102
102
  <meta name="viewport" content="width=device-width, initial-scale=1">
103
103
  <meta name="robots" content="noindex">
104
+ <link rel="icon" href="data:,">
104
105
  <title>Station · nixamp</title>
105
106
  <style>${STYLE}</style>
106
107
  </head>
@@ -137,7 +138,7 @@ export const STATION_PAGE_CSP = [
137
138
  `style-src ${hash(STYLE)}`,
138
139
  "connect-src 'self'",
139
140
  "media-src 'self'",
140
- "img-src 'self'",
141
+ "img-src 'self' data:",
141
142
  "base-uri 'none'",
142
143
  "form-action 'none'",
143
144
  "frame-ancestors 'none'",
@@ -76,7 +76,8 @@ function nowPlayingFile(ctx: StationContext, station: Station): string {
76
76
  export function publicView(ctx: StationContext, station: Station): PublicStation {
77
77
  const state = ctx.runner.observed(station.id);
78
78
  const file = state === "live" || state === "recovering" ? nowPlayingFile(ctx, station) : "";
79
- const track = file ? ctx.trackFor(file) : null;
79
+ // The tags the player read when it put this file on, else the library's.
80
+ const track = file ? (ctx.channels.tagsOf(channelOf(station.id), file) ?? ctx.trackFor(file)) : null;
80
81
  return {
81
82
  id: station.id,
82
83
  name: station.name,
@@ -2,7 +2,7 @@
2
2
  export const USAGE = `nixamp station — make a collection into a station
3
3
 
4
4
  nixamp station list
5
- nixamp station create --name NAME (--folder PATH | --items 0,1,2) [--shuffle] [--no-repeat] [--bitrate 128]
5
+ nixamp station create --name NAME (--folder PATH | --playlist ID | --items 0,1,2) [--shuffle] [--no-repeat] [--bitrate 128]
6
6
  nixamp station show ID
7
7
  nixamp station status ID
8
8
  nixamp station start ID
@@ -1 +1 @@
1
- import{n as e}from"./index-7mCQZGNF.js";var t={...e,"No parties live right now.":`لا توجد جلسات مباشرة الآن.`,"Could not refresh parties.":`تعذر تحديث الجلسات.`,"Pick a server in the directory, or paste its link here.":`اختر خادمًا من الدليل أو الصق رابطه هنا.`,"Interface language":`لغة الواجهة`,Language:`اللغة`,"Translation language":`لغة الترجمة`,"Original (auto-detect)":`الأصل (اكتشاف تلقائي)`,Play:`تشغيل`,Pause:`إيقاف مؤقت`,Stop:`إيقاف`,Next:`التالي`,Previous:`السابق`,Select:`اختيار`,"Next/Prev":`التالي/السابق`,Quit:`خروج`,"Now Playing":`قيد التشغيل`,"Spectrum Analyser":`محلل الطيف`,Playlist:`قائمة التشغيل`,Files:`الملفات`,"Open files":`فتح ملفات`,"Open folder":`فتح مجلد`,"Add to library":`إضافة إلى المكتبة`,"Replace playlist":`استبدال قائمة التشغيل`,Filter:`تصفية`,Search:`بحث`,"Nothing loaded.":`لم يُحمّل أي محتوى.`,"Empty.":`فارغ.`,"Loading…":`جارٍ التحميل…`,"Connecting…":`جارٍ الاتصال…`,Connected:`متصل`,"Not connected":`غير متصل`,"No source":`لا يوجد مصدر`,Playing:`يعمل`,Stopped:`متوقف`,Volume:`مستوى الصوت`,Seek:`موضع التشغيل`,"Full screen":`ملء الشاشة`,Download:`تنزيل`,Share:`مشاركة`,Copy:`نسخ`,"Copy link":`نسخ الرابط`,Copied:`تم النسخ`,Save:`حفظ`,Cancel:`إلغاء`,Close:`إغلاق`,Remove:`إزالة`,Delete:`حذف`,Edit:`تعديل`,Add:`إضافة`,Send:`إرسال`,Retry:`إعادة المحاولة`,Back:`رجوع`,"Next page":`الصفحة التالية`,"Previous page":`الصفحة السابقة`,"Show more":`عرض المزيد`,Panels:`اللوحات`,"Reset layout":`إعادة ضبط التخطيط`,Show:`إظهار`,Hide:`إخفاء`,Collapse:`طي`,Expand:`توسيع`,"Move up":`نقل لأعلى`,"Move down":`نقل لأسفل`,Install:`تثبيت`,"TV mode":`وضع التلفاز`,On:`مفعّل`,Off:`معطّل`,Account:`الحساب`,"Sign in":`تسجيل الدخول`,"Sign out":`تسجيل الخروج`,"Create account":`إنشاء حساب`,Email:`البريد الإلكتروني`,Password:`كلمة المرور`,"Forgot password?":`نسيت كلمة المرور؟`,"Welcome back":`مرحبًا بعودتك`,Profile:`الملف الشخصي`,Handle:`اسم المستخدم`,"Your servers":`خوادمك`,Server:`الخادم`,Connect:`اتصال`,Disconnect:`قطع الاتصال`,Directory:`الدليل`,Favourites:`المفضلة`,Follow:`متابعة`,Unfollow:`إلغاء المتابعة`,Notifications:`الإشعارات`,"Phone number":`رقم الهاتف`,"Save number":`حفظ الرقم`,Transcript:`النص المفرغ`,Captions:`الترجمة النصية`,"Translate audio":`ترجمة الصوت`,"Audio language":`لغة الصوت`,"Audio options":`خيارات الصوت`,"Keep background sound":`الاحتفاظ بأصوات الخلفية`,"Background level":`مستوى صوت الخلفية`,"Speaker voices":`أصوات المتحدثين`,"Any voice":`أي صوت`,"Male voice":`صوت رجالي`,"Female voice":`صوت نسائي`,"Original audio.":`الصوت الأصلي.`,"Buy translated audio":`شراء الصوت المترجم`,"Audio credit":`رصيد الصوت`,"Buy pass":`شراء تصريح`,Pass:`تصريح`,"Pay with":`الدفع باستخدام`,"Check payment":`التحقق من الدفع`,"Open checkout":`الانتقال للدفع`,"Free sessions":`جلسات مجانية`,"Usage rates and expiry":`الأسعار ومدة الصلاحية`,Catalogs:`الكتالوجات`,"Add catalog":`إضافة كتالوج`,"Catalog name":`اسم الكتالوج`,Link:`الرابط`,"Play link":`تشغيل الرابط`,"Go live":`بدء بث مباشر`,Publish:`نشر`,Listen:`استماع`,"Live now":`مباشر الآن`,Upcoming:`قريبًا`,Parties:`حفلات المشاهدة`,"Join party":`الانضمام للحفلة`,"Invite code":`رمز الدعوة`,Invite:`دعوة`,Chat:`الدردشة`,"Chat message":`رسالة دردشة`,People:`الأشخاص`,Speaker:`متحدث`,Speakers:`المتحدثون`,Listener:`مستمع`,Moderator:`مشرف`,"Raise hand":`رفع اليد`,"Hand raises":`الأيدي المرفوعة`,Questions:`الأسئلة`,Resources:`الموارد`,Replay:`إعادة العرض`,Schedule:`الجدول`,Event:`الفعالية`,Host:`المضيف`,About:`حول`,Stage:`المنصة`,Title:`العنوان`,Description:`الوصف`,Topic:`الموضوع`,Public:`عام`,"Anyone with the link":`أي شخص لديه الرابط`,"Invited people":`الأشخاص المدعوون`,"Who can join":`من يمكنه الانضمام`,"Date and time":`التاريخ والوقت`,"Expected length":`المدة المتوقعة`,Repeat:`تكرار`,"Does not repeat":`لا يتكرر`,"Every day":`كل يوم`,"Every week":`كل أسبوع`,Homepage:`الموقع الإلكتروني`,"Host name":`اسم المضيف`,"Profile photo URL":`رابط صورة الملف الشخصي`,"Broadcast link":`رابط البث`,"Edit event":`تعديل الفعالية`,"Save changes":`حفظ التغييرات`,"Create live":`إنشاء بث مباشر`,"Schedule class":`جدولة درس`,"Start class":`بدء الدرس`,"End event":`إنهاء الفعالية`,"End session":`إنهاء الجلسة`,"Session ended":`انتهت الجلسة`,"Event ended":`انتهت الفعالية`,Cancelled:`ملغاة`,"Replay available":`إعادة العرض متاحة`,Join:`انضمام`,"Create invite":`إنشاء دعوة`,"Invite as":`الدعوة بصفة`,"Watch now":`شاهد الآن`,"Start a room":`إنشاء غرفة`,"Recent replays":`إعادات العرض الأخيرة`,"Your account":`حسابك`,"Write with AI":`الكتابة بالذكاء الاصطناعي`,"Suggested title":`العنوان المقترح`,"Suggested description":`الوصف المقترح`,"Suggested topic":`الموضوع المقترح`,"Use these details":`استخدام هذه التفاصيل`,"Learn something live.":`تعلّم شيئًا مباشرة.`,"Screen share":`مشاركة الشاشة`,"Upload and broadcast":`رفع وبث`,Help:`مساعدة`,"Reload Player":`إعادة تحميل المشغل`,"Copy Bundled CLI Path":`نسخ مسار أداة سطر الأوامر المضمنة`,"Developer tools":`أدوات المطور`,Undo:`تراجع`,Redo:`إعادة`,Cut:`قص`,Paste:`لصق`,"Select all":`تحديد الكل`,Status:`الحالة`,"Activity log":`سجل النشاط`,Admin:`الإدارة`,"Playback controls":`عناصر التحكم بالتشغيل`,"Tracks and folders":`المقاطع والمجلدات`,"Original-language captions.":`ترجمة نصية باللغة الأصلية.`,"Skip to playback controls":`الانتقال إلى عناصر التحكم بالتشغيل`,"Skip to playlist":`الانتقال إلى قائمة التشغيل`,"Skip to transcript and translated audio":`الانتقال إلى النص المفرغ والصوت المترجم`};export{t as default};
1
+ import{n as e}from"./index-D-PZ4nGC.js";var t={...e,"No parties live right now.":`لا توجد جلسات مباشرة الآن.`,"Could not refresh parties.":`تعذر تحديث الجلسات.`,"Pick a server in the directory, or paste its link here.":`اختر خادمًا من الدليل أو الصق رابطه هنا.`,"Interface language":`لغة الواجهة`,Language:`اللغة`,"Translation language":`لغة الترجمة`,"Original (auto-detect)":`الأصل (اكتشاف تلقائي)`,Play:`تشغيل`,Pause:`إيقاف مؤقت`,Stop:`إيقاف`,Next:`التالي`,Previous:`السابق`,Select:`اختيار`,"Next/Prev":`التالي/السابق`,Quit:`خروج`,"Now Playing":`قيد التشغيل`,"Spectrum Analyser":`محلل الطيف`,Playlist:`قائمة التشغيل`,Files:`الملفات`,"Open files":`فتح ملفات`,"Open folder":`فتح مجلد`,"Add to library":`إضافة إلى المكتبة`,"Replace playlist":`استبدال قائمة التشغيل`,Filter:`تصفية`,Search:`بحث`,"Nothing loaded.":`لم يُحمّل أي محتوى.`,"Empty.":`فارغ.`,"Loading…":`جارٍ التحميل…`,"Connecting…":`جارٍ الاتصال…`,Connected:`متصل`,"Not connected":`غير متصل`,"No source":`لا يوجد مصدر`,Playing:`يعمل`,Stopped:`متوقف`,Volume:`مستوى الصوت`,Seek:`موضع التشغيل`,"Full screen":`ملء الشاشة`,Download:`تنزيل`,Share:`مشاركة`,Copy:`نسخ`,"Copy link":`نسخ الرابط`,Copied:`تم النسخ`,Save:`حفظ`,Cancel:`إلغاء`,Close:`إغلاق`,Remove:`إزالة`,Delete:`حذف`,Edit:`تعديل`,Add:`إضافة`,Send:`إرسال`,Retry:`إعادة المحاولة`,Back:`رجوع`,"Next page":`الصفحة التالية`,"Previous page":`الصفحة السابقة`,"Show more":`عرض المزيد`,Panels:`اللوحات`,"Reset layout":`إعادة ضبط التخطيط`,Show:`إظهار`,Hide:`إخفاء`,Collapse:`طي`,Expand:`توسيع`,"Move up":`نقل لأعلى`,"Move down":`نقل لأسفل`,Install:`تثبيت`,"TV mode":`وضع التلفاز`,On:`مفعّل`,Off:`معطّل`,Account:`الحساب`,"Sign in":`تسجيل الدخول`,"Sign out":`تسجيل الخروج`,"Create account":`إنشاء حساب`,Email:`البريد الإلكتروني`,Password:`كلمة المرور`,"Forgot password?":`نسيت كلمة المرور؟`,"Welcome back":`مرحبًا بعودتك`,Profile:`الملف الشخصي`,Handle:`اسم المستخدم`,"Your servers":`خوادمك`,Server:`الخادم`,Connect:`اتصال`,Disconnect:`قطع الاتصال`,Directory:`الدليل`,Favourites:`المفضلة`,Follow:`متابعة`,Unfollow:`إلغاء المتابعة`,Notifications:`الإشعارات`,"Phone number":`رقم الهاتف`,"Save number":`حفظ الرقم`,Transcript:`النص المفرغ`,Captions:`الترجمة النصية`,"Translate audio":`ترجمة الصوت`,"Audio language":`لغة الصوت`,"Audio options":`خيارات الصوت`,"Keep background sound":`الاحتفاظ بأصوات الخلفية`,"Background level":`مستوى صوت الخلفية`,"Speaker voices":`أصوات المتحدثين`,"Any voice":`أي صوت`,"Male voice":`صوت رجالي`,"Female voice":`صوت نسائي`,"Original audio.":`الصوت الأصلي.`,"Buy translated audio":`شراء الصوت المترجم`,"Audio credit":`رصيد الصوت`,"Buy pass":`شراء تصريح`,Pass:`تصريح`,"Pay with":`الدفع باستخدام`,"Check payment":`التحقق من الدفع`,"Open checkout":`الانتقال للدفع`,"Free sessions":`جلسات مجانية`,"Usage rates and expiry":`الأسعار ومدة الصلاحية`,Catalogs:`الكتالوجات`,"Add catalog":`إضافة كتالوج`,"Catalog name":`اسم الكتالوج`,Link:`الرابط`,"Play link":`تشغيل الرابط`,"Go live":`بدء بث مباشر`,Publish:`نشر`,Listen:`استماع`,"Live now":`مباشر الآن`,Upcoming:`قريبًا`,Parties:`حفلات المشاهدة`,"Join party":`الانضمام للحفلة`,"Invite code":`رمز الدعوة`,Invite:`دعوة`,Chat:`الدردشة`,"Chat message":`رسالة دردشة`,People:`الأشخاص`,Speaker:`متحدث`,Speakers:`المتحدثون`,Listener:`مستمع`,Moderator:`مشرف`,"Raise hand":`رفع اليد`,"Hand raises":`الأيدي المرفوعة`,Questions:`الأسئلة`,Resources:`الموارد`,Replay:`إعادة العرض`,Schedule:`الجدول`,Event:`الفعالية`,Host:`المضيف`,About:`حول`,Stage:`المنصة`,Title:`العنوان`,Description:`الوصف`,Topic:`الموضوع`,Public:`عام`,"Anyone with the link":`أي شخص لديه الرابط`,"Invited people":`الأشخاص المدعوون`,"Who can join":`من يمكنه الانضمام`,"Date and time":`التاريخ والوقت`,"Expected length":`المدة المتوقعة`,Repeat:`تكرار`,"Does not repeat":`لا يتكرر`,"Every day":`كل يوم`,"Every week":`كل أسبوع`,Homepage:`الموقع الإلكتروني`,"Host name":`اسم المضيف`,"Profile photo URL":`رابط صورة الملف الشخصي`,"Broadcast link":`رابط البث`,"Edit event":`تعديل الفعالية`,"Save changes":`حفظ التغييرات`,"Create live":`إنشاء بث مباشر`,"Schedule class":`جدولة درس`,"Start class":`بدء الدرس`,"End event":`إنهاء الفعالية`,"End session":`إنهاء الجلسة`,"Session ended":`انتهت الجلسة`,"Event ended":`انتهت الفعالية`,Cancelled:`ملغاة`,"Replay available":`إعادة العرض متاحة`,Join:`انضمام`,"Create invite":`إنشاء دعوة`,"Invite as":`الدعوة بصفة`,"Watch now":`شاهد الآن`,"Start a room":`إنشاء غرفة`,"Recent replays":`إعادات العرض الأخيرة`,"Your account":`حسابك`,"Write with AI":`الكتابة بالذكاء الاصطناعي`,"Suggested title":`العنوان المقترح`,"Suggested description":`الوصف المقترح`,"Suggested topic":`الموضوع المقترح`,"Use these details":`استخدام هذه التفاصيل`,"Learn something live.":`تعلّم شيئًا مباشرة.`,"Screen share":`مشاركة الشاشة`,"Upload and broadcast":`رفع وبث`,Help:`مساعدة`,"Reload Player":`إعادة تحميل المشغل`,"Copy Bundled CLI Path":`نسخ مسار أداة سطر الأوامر المضمنة`,"Developer tools":`أدوات المطور`,Undo:`تراجع`,Redo:`إعادة`,Cut:`قص`,Paste:`لصق`,"Select all":`تحديد الكل`,Status:`الحالة`,"Activity log":`سجل النشاط`,Admin:`الإدارة`,"Playback controls":`عناصر التحكم بالتشغيل`,"Tracks and folders":`المقاطع والمجلدات`,"Original-language captions.":`ترجمة نصية باللغة الأصلية.`,"Skip to playback controls":`الانتقال إلى عناصر التحكم بالتشغيل`,"Skip to playlist":`الانتقال إلى قائمة التشغيل`,"Skip to transcript and translated audio":`الانتقال إلى النص المفرغ والصوت المترجم`};export{t as default};
@@ -1 +1 @@
1
- import{n as e}from"./index-7mCQZGNF.js";var t={...e,"No parties live right now.":`Právě neprobíhají žádné párty.`,"Could not refresh parties.":`Párty se nepodařilo aktualizovat.`,"Pick a server in the directory, or paste its link here.":`Vyberte server v seznamu nebo sem vložte jeho odkaz.`,"Interface language":`Jazyk rozhraní`,Language:`Jazyk`,"Translation language":`Jazyk překladu`,"Original (auto-detect)":`Původní (automatické rozpoznání)`,Play:`Přehrát`,Pause:`Pozastavit`,Stop:`Zastavit`,Next:`Další`,Previous:`Předchozí`,Select:`Vybrat`,"Next/Prev":`Další/Předchozí`,Quit:`Ukončit`,"Now Playing":`Právě hraje`,"Spectrum Analyser":`Spektrální analyzátor`,Playlist:`Seznam skladeb`,Files:`Soubory`,"Open files":`Otevřít soubory`,"Open folder":`Otevřít složku`,"Add to library":`Přidat do knihovny`,"Replace playlist":`Nahradit seznam`,Filter:`Filtrovat`,Search:`Hledat`,"Nothing loaded.":`Nic není načteno.`,"Empty.":`Prázdné.`,"Loading…":`Načítání…`,"Connecting…":`Připojování…`,Connected:`Připojeno`,"Not connected":`Nepřipojeno`,"No source":`Žádný zdroj`,Playing:`Přehrávání`,Stopped:`Zastaveno`,Volume:`Hlasitost`,Seek:`Pozice přehrávání`,"Full screen":`Celá obrazovka`,Download:`Stáhnout`,Share:`Sdílet`,Copy:`Kopírovat`,"Copy link":`Kopírovat odkaz`,Copied:`Zkopírováno`,Save:`Uložit`,Cancel:`Zrušit`,Close:`Zavřít`,Remove:`Odebrat`,Delete:`Smazat`,Edit:`Upravit`,Add:`Přidat`,Send:`Odeslat`,Retry:`Zkusit znovu`,Back:`Zpět`,"Next page":`Další stránka`,"Previous page":`Předchozí stránka`,"Show more":`Zobrazit více`,Panels:`Panely`,"Reset layout":`Obnovit rozložení`,Show:`Zobrazit`,Hide:`Skrýt`,Collapse:`Sbalit`,Expand:`Rozbalit`,"Move up":`Nahoru`,"Move down":`Dolů`,Install:`Nainstalovat`,"TV mode":`Režim TV`,On:`Zapnuto`,Off:`Vypnuto`,Account:`Účet`,"Sign in":`Přihlásit se`,"Sign out":`Odhlásit se`,"Create account":`Vytvořit účet`,Email:`E-mail`,Password:`Heslo`,"Forgot password?":`Zapomenuté heslo?`,"Welcome back":`Vítejte zpět`,Profile:`Profil`,Handle:`Uživatelské jméno`,"Your servers":`Vaše servery`,Server:`Server`,Connect:`Připojit`,Disconnect:`Odpojit`,Directory:`Adresář`,Favourites:`Oblíbené`,Follow:`Sledovat`,Unfollow:`Přestat sledovat`,Notifications:`Oznámení`,"Phone number":`Telefonní číslo`,"Save number":`Uložit číslo`,Transcript:`Přepis`,Captions:`Titulky`,"Translate audio":`Překládat zvuk`,"Audio language":`Jazyk zvuku`,"Audio options":`Možnosti zvuku`,"Keep background sound":`Zachovat zvuk na pozadí`,"Background level":`Hlasitost pozadí`,"Speaker voices":`Hlasy mluvčích`,"Any voice":`Libovolný hlas`,"Male voice":`Mužský hlas`,"Female voice":`Ženský hlas`,"Original audio.":`Původní zvuk.`,"Buy translated audio":`Koupit překlad zvuku`,"Audio credit":`Zvukový kredit`,"Buy pass":`Koupit přístup`,Pass:`Přístup`,"Pay with":`Zaplatit pomocí`,"Check payment":`Ověřit platbu`,"Open checkout":`Přejít k platbě`,"Free sessions":`Bezplatné relace`,"Usage rates and expiry":`Ceny a platnost`,Catalogs:`Katalogy`,"Add catalog":`Přidat katalog`,"Catalog name":`Název katalogu`,Link:`Odkaz`,"Play link":`Přehrát odkaz`,"Go live":`Vysílat živě`,Publish:`Zveřejnit`,Listen:`Poslouchat`,"Live now":`Právě živě`,Upcoming:`Připravované`,Parties:`Společná sledování`,"Join party":`Připojit se ke sledování`,"Invite code":`Kód pozvánky`,Invite:`Pozvat`,Chat:`Chat`,"Chat message":`Zpráva chatu`,People:`Lidé`,Speaker:`Mluvčí`,Speakers:`Mluvčí`,Listener:`Posluchač`,Moderator:`Moderátor`,"Raise hand":`Zvednout ruku`,"Hand raises":`Zvednuté ruce`,Questions:`Otázky`,Resources:`Materiály`,Replay:`Záznam`,Schedule:`Rozvrh`,Event:`Událost`,Host:`Pořadatel`,About:`O události`,Stage:`Pódium`,Title:`Název`,Description:`Popis`,Topic:`Téma`,Public:`Veřejné`,"Anyone with the link":`Kdokoli s odkazem`,"Invited people":`Pozvané osoby`,"Who can join":`Kdo se může připojit`,"Date and time":`Datum a čas`,"Expected length":`Očekávaná délka`,Repeat:`Opakovat`,"Does not repeat":`Neopakuje se`,"Every day":`Každý den`,"Every week":`Každý týden`,Homepage:`Webové stránky`,"Host name":`Jméno pořadatele`,"Profile photo URL":`URL profilové fotky`,"Broadcast link":`Odkaz na vysílání`,"Edit event":`Upravit událost`,"Save changes":`Uložit změny`,"Create live":`Vytvořit vysílání`,"Schedule class":`Naplánovat lekci`,"Start class":`Zahájit lekci`,"End event":`Ukončit událost`,"End session":`Ukončit relaci`,"Session ended":`Relace ukončena`,"Event ended":`Událost ukončena`,Cancelled:`Zrušeno`,"Replay available":`Záznam je k dispozici`,Join:`Připojit se`,"Create invite":`Vytvořit pozvánku`,"Invite as":`Pozvat jako`,"Watch now":`Sledovat nyní`,"Start a room":`Vytvořit místnost`,"Recent replays":`Nedávné záznamy`,"Your account":`Váš účet`,"Write with AI":`Psát pomocí AI`,"Suggested title":`Navrhovaný název`,"Suggested description":`Navrhovaný popis`,"Suggested topic":`Navrhované téma`,"Use these details":`Použít tyto údaje`,"Learn something live.":`Učte se naživo.`,"Screen share":`Sdílet obrazovku`,"Upload and broadcast":`Nahrát a vysílat`,Help:`Nápověda`,"Reload Player":`Znovu načíst přehrávač`,"Copy Bundled CLI Path":`Kopírovat cestu k přibalenému CLI`,"Developer tools":`Vývojářské nástroje`,Undo:`Zpět`,Redo:`Znovu`,Cut:`Vyjmout`,Paste:`Vložit`,"Select all":`Vybrat vše`,Status:`Stav`,"Activity log":`Protokol aktivit`,Admin:`Správa`,"Playback controls":`Ovládání přehrávání`,"Tracks and folders":`Skladby a složky`,"Original-language captions.":`Titulky v původním jazyce.`,"Skip to playback controls":`Přejít k ovládání přehrávání`,"Skip to playlist":`Přejít k seznamu skladeb`,"Skip to transcript and translated audio":`Přejít k přepisu a přeloženému zvuku`};export{t as default};
1
+ import{n as e}from"./index-D-PZ4nGC.js";var t={...e,"No parties live right now.":`Právě neprobíhají žádné párty.`,"Could not refresh parties.":`Párty se nepodařilo aktualizovat.`,"Pick a server in the directory, or paste its link here.":`Vyberte server v seznamu nebo sem vložte jeho odkaz.`,"Interface language":`Jazyk rozhraní`,Language:`Jazyk`,"Translation language":`Jazyk překladu`,"Original (auto-detect)":`Původní (automatické rozpoznání)`,Play:`Přehrát`,Pause:`Pozastavit`,Stop:`Zastavit`,Next:`Další`,Previous:`Předchozí`,Select:`Vybrat`,"Next/Prev":`Další/Předchozí`,Quit:`Ukončit`,"Now Playing":`Právě hraje`,"Spectrum Analyser":`Spektrální analyzátor`,Playlist:`Seznam skladeb`,Files:`Soubory`,"Open files":`Otevřít soubory`,"Open folder":`Otevřít složku`,"Add to library":`Přidat do knihovny`,"Replace playlist":`Nahradit seznam`,Filter:`Filtrovat`,Search:`Hledat`,"Nothing loaded.":`Nic není načteno.`,"Empty.":`Prázdné.`,"Loading…":`Načítání…`,"Connecting…":`Připojování…`,Connected:`Připojeno`,"Not connected":`Nepřipojeno`,"No source":`Žádný zdroj`,Playing:`Přehrávání`,Stopped:`Zastaveno`,Volume:`Hlasitost`,Seek:`Pozice přehrávání`,"Full screen":`Celá obrazovka`,Download:`Stáhnout`,Share:`Sdílet`,Copy:`Kopírovat`,"Copy link":`Kopírovat odkaz`,Copied:`Zkopírováno`,Save:`Uložit`,Cancel:`Zrušit`,Close:`Zavřít`,Remove:`Odebrat`,Delete:`Smazat`,Edit:`Upravit`,Add:`Přidat`,Send:`Odeslat`,Retry:`Zkusit znovu`,Back:`Zpět`,"Next page":`Další stránka`,"Previous page":`Předchozí stránka`,"Show more":`Zobrazit více`,Panels:`Panely`,"Reset layout":`Obnovit rozložení`,Show:`Zobrazit`,Hide:`Skrýt`,Collapse:`Sbalit`,Expand:`Rozbalit`,"Move up":`Nahoru`,"Move down":`Dolů`,Install:`Nainstalovat`,"TV mode":`Režim TV`,On:`Zapnuto`,Off:`Vypnuto`,Account:`Účet`,"Sign in":`Přihlásit se`,"Sign out":`Odhlásit se`,"Create account":`Vytvořit účet`,Email:`E-mail`,Password:`Heslo`,"Forgot password?":`Zapomenuté heslo?`,"Welcome back":`Vítejte zpět`,Profile:`Profil`,Handle:`Uživatelské jméno`,"Your servers":`Vaše servery`,Server:`Server`,Connect:`Připojit`,Disconnect:`Odpojit`,Directory:`Adresář`,Favourites:`Oblíbené`,Follow:`Sledovat`,Unfollow:`Přestat sledovat`,Notifications:`Oznámení`,"Phone number":`Telefonní číslo`,"Save number":`Uložit číslo`,Transcript:`Přepis`,Captions:`Titulky`,"Translate audio":`Překládat zvuk`,"Audio language":`Jazyk zvuku`,"Audio options":`Možnosti zvuku`,"Keep background sound":`Zachovat zvuk na pozadí`,"Background level":`Hlasitost pozadí`,"Speaker voices":`Hlasy mluvčích`,"Any voice":`Libovolný hlas`,"Male voice":`Mužský hlas`,"Female voice":`Ženský hlas`,"Original audio.":`Původní zvuk.`,"Buy translated audio":`Koupit překlad zvuku`,"Audio credit":`Zvukový kredit`,"Buy pass":`Koupit přístup`,Pass:`Přístup`,"Pay with":`Zaplatit pomocí`,"Check payment":`Ověřit platbu`,"Open checkout":`Přejít k platbě`,"Free sessions":`Bezplatné relace`,"Usage rates and expiry":`Ceny a platnost`,Catalogs:`Katalogy`,"Add catalog":`Přidat katalog`,"Catalog name":`Název katalogu`,Link:`Odkaz`,"Play link":`Přehrát odkaz`,"Go live":`Vysílat živě`,Publish:`Zveřejnit`,Listen:`Poslouchat`,"Live now":`Právě živě`,Upcoming:`Připravované`,Parties:`Společná sledování`,"Join party":`Připojit se ke sledování`,"Invite code":`Kód pozvánky`,Invite:`Pozvat`,Chat:`Chat`,"Chat message":`Zpráva chatu`,People:`Lidé`,Speaker:`Mluvčí`,Speakers:`Mluvčí`,Listener:`Posluchač`,Moderator:`Moderátor`,"Raise hand":`Zvednout ruku`,"Hand raises":`Zvednuté ruce`,Questions:`Otázky`,Resources:`Materiály`,Replay:`Záznam`,Schedule:`Rozvrh`,Event:`Událost`,Host:`Pořadatel`,About:`O události`,Stage:`Pódium`,Title:`Název`,Description:`Popis`,Topic:`Téma`,Public:`Veřejné`,"Anyone with the link":`Kdokoli s odkazem`,"Invited people":`Pozvané osoby`,"Who can join":`Kdo se může připojit`,"Date and time":`Datum a čas`,"Expected length":`Očekávaná délka`,Repeat:`Opakovat`,"Does not repeat":`Neopakuje se`,"Every day":`Každý den`,"Every week":`Každý týden`,Homepage:`Webové stránky`,"Host name":`Jméno pořadatele`,"Profile photo URL":`URL profilové fotky`,"Broadcast link":`Odkaz na vysílání`,"Edit event":`Upravit událost`,"Save changes":`Uložit změny`,"Create live":`Vytvořit vysílání`,"Schedule class":`Naplánovat lekci`,"Start class":`Zahájit lekci`,"End event":`Ukončit událost`,"End session":`Ukončit relaci`,"Session ended":`Relace ukončena`,"Event ended":`Událost ukončena`,Cancelled:`Zrušeno`,"Replay available":`Záznam je k dispozici`,Join:`Připojit se`,"Create invite":`Vytvořit pozvánku`,"Invite as":`Pozvat jako`,"Watch now":`Sledovat nyní`,"Start a room":`Vytvořit místnost`,"Recent replays":`Nedávné záznamy`,"Your account":`Váš účet`,"Write with AI":`Psát pomocí AI`,"Suggested title":`Navrhovaný název`,"Suggested description":`Navrhovaný popis`,"Suggested topic":`Navrhované téma`,"Use these details":`Použít tyto údaje`,"Learn something live.":`Učte se naživo.`,"Screen share":`Sdílet obrazovku`,"Upload and broadcast":`Nahrát a vysílat`,Help:`Nápověda`,"Reload Player":`Znovu načíst přehrávač`,"Copy Bundled CLI Path":`Kopírovat cestu k přibalenému CLI`,"Developer tools":`Vývojářské nástroje`,Undo:`Zpět`,Redo:`Znovu`,Cut:`Vyjmout`,Paste:`Vložit`,"Select all":`Vybrat vše`,Status:`Stav`,"Activity log":`Protokol aktivit`,Admin:`Správa`,"Playback controls":`Ovládání přehrávání`,"Tracks and folders":`Skladby a složky`,"Original-language captions.":`Titulky v původním jazyce.`,"Skip to playback controls":`Přejít k ovládání přehrávání`,"Skip to playlist":`Přejít k seznamu skladeb`,"Skip to transcript and translated audio":`Přejít k přepisu a přeloženému zvuku`};export{t as default};
@@ -1 +1 @@
1
- import{n as e}from"./index-7mCQZGNF.js";var t={...e,"No parties live right now.":`Ingen fester er live lige nu.`,"Could not refresh parties.":`Kunne ikke opdatere festerne.`,"Pick a server in the directory, or paste its link here.":`Vælg en server i oversigten, eller indsæt dens link her.`,"Interface language":`Grænsefladesprog`,Language:`Sprog`,"Translation language":`Oversættelsessprog`,"Original (auto-detect)":`Original (automatisk registrering)`,Play:`Afspil`,Pause:`Pause`,Stop:`Stop`,Next:`Næste`,Previous:`Forrige`,Select:`Vælg`,"Next/Prev":`Næste/Forrige`,Quit:`Afslut`,"Now Playing":`Afspilles nu`,"Spectrum Analyser":`Spektrumanalysator`,Playlist:`Afspilningsliste`,Files:`Filer`,"Open files":`Åbn filer`,"Open folder":`Åbn mappe`,"Add to library":`Føj til bibliotek`,"Replace playlist":`Erstat afspilningsliste`,Filter:`Filtrer`,Search:`Søg`,"Nothing loaded.":`Intet indlæst.`,"Empty.":`Tomt.`,"Loading…":`Indlæser…`,"Connecting…":`Forbinder…`,Connected:`Forbundet`,"Not connected":`Ikke forbundet`,"No source":`Ingen kilde`,Playing:`Afspiller`,Stopped:`Stoppet`,Volume:`Lydstyrke`,Seek:`Afspilningsposition`,"Full screen":`Fuld skærm`,Download:`Download`,Share:`Del`,Copy:`Kopiér`,"Copy link":`Kopiér link`,Copied:`Kopieret`,Save:`Gem`,Cancel:`Annuller`,Close:`Luk`,Remove:`Fjern`,Delete:`Slet`,Edit:`Rediger`,Add:`Tilføj`,Send:`Send`,Retry:`Prøv igen`,Back:`Tilbage`,"Next page":`Næste side`,"Previous page":`Forrige side`,"Show more":`Vis flere`,Panels:`Paneler`,"Reset layout":`Nulstil layout`,Show:`Vis`,Hide:`Skjul`,Collapse:`Fold sammen`,Expand:`Fold ud`,"Move up":`Flyt op`,"Move down":`Flyt ned`,Install:`Installer`,"TV mode":`TV-tilstand`,On:`Til`,Off:`Fra`,Account:`Konto`,"Sign in":`Log ind`,"Sign out":`Log ud`,"Create account":`Opret konto`,Email:`E-mail`,Password:`Adgangskode`,"Forgot password?":`Glemt adgangskode?`,"Welcome back":`Velkommen tilbage`,Profile:`Profil`,Handle:`Brugernavn`,"Your servers":`Dine servere`,Server:`Server`,Connect:`Forbind`,Disconnect:`Afbryd`,Directory:`Oversigt`,Favourites:`Favoritter`,Follow:`Følg`,Unfollow:`Følg ikke længere`,Notifications:`Notifikationer`,"Phone number":`Telefonnummer`,"Save number":`Gem nummer`,Transcript:`Transskription`,Captions:`Undertekster`,"Translate audio":`Oversæt lyd`,"Audio language":`Lydsprog`,"Audio options":`Lydindstillinger`,"Keep background sound":`Bevar baggrundslyd`,"Background level":`Baggrundslydstyrke`,"Speaker voices":`Talernes stemmer`,"Any voice":`Vilkårlig stemme`,"Male voice":`Mandlig stemme`,"Female voice":`Kvindelig stemme`,"Original audio.":`Original lyd.`,"Buy translated audio":`Køb oversat lyd`,"Audio credit":`Lydsaldo`,"Buy pass":`Køb adgang`,Pass:`Adgang`,"Pay with":`Betal med`,"Check payment":`Kontrollér betaling`,"Open checkout":`Gå til betaling`,"Free sessions":`Gratis sessioner`,"Usage rates and expiry":`Priser og udløb`,Catalogs:`Kataloger`,"Add catalog":`Tilføj katalog`,"Catalog name":`Katalognavn`,Link:`Link`,"Play link":`Afspil link`,"Go live":`Gå live`,Publish:`Udgiv`,Listen:`Lyt`,"Live now":`Live nu`,Upcoming:`Kommende`,Parties:`Seerfester`,"Join party":`Deltag i festen`,"Invite code":`Invitationskode`,Invite:`Inviter`,Chat:`Chat`,"Chat message":`Chatbesked`,People:`Personer`,Speaker:`Taler`,Speakers:`Talere`,Listener:`Lytter`,Moderator:`Moderator`,"Raise hand":`Ræk hånden op`,"Hand raises":`Oprakte hænder`,Questions:`Spørgsmål`,Resources:`Materialer`,Replay:`Genudsendelse`,Schedule:`Program`,Event:`Begivenhed`,Host:`Vært`,About:`Om`,Stage:`Scene`,Title:`Titel`,Description:`Beskrivelse`,Topic:`Emne`,Public:`Offentlig`,"Anyone with the link":`Alle med linket`,"Invited people":`Inviterede personer`,"Who can join":`Hvem kan deltage`,"Date and time":`Dato og klokkeslæt`,"Expected length":`Forventet varighed`,Repeat:`Gentag`,"Does not repeat":`Gentages ikke`,"Every day":`Hver dag`,"Every week":`Hver uge`,Homepage:`Hjemmeside`,"Host name":`Værtens navn`,"Profile photo URL":`URL til profilbillede`,"Broadcast link":`Udsendelseslink`,"Edit event":`Rediger begivenhed`,"Save changes":`Gem ændringer`,"Create live":`Opret livesending`,"Schedule class":`Planlæg undervisning`,"Start class":`Start undervisning`,"End event":`Afslut begivenhed`,"End session":`Afslut session`,"Session ended":`Session afsluttet`,"Event ended":`Begivenhed afsluttet`,Cancelled:`Annulleret`,"Replay available":`Genudsendelse tilgængelig`,Join:`Deltag`,"Create invite":`Opret invitation`,"Invite as":`Inviter som`,"Watch now":`Se nu`,"Start a room":`Opret et rum`,"Recent replays":`Seneste genudsendelser`,"Your account":`Din konto`,"Write with AI":`Skriv med AI`,"Suggested title":`Foreslået titel`,"Suggested description":`Foreslået beskrivelse`,"Suggested topic":`Foreslået emne`,"Use these details":`Brug disse oplysninger`,"Learn something live.":`Lær noget live.`,"Screen share":`Del skærm`,"Upload and broadcast":`Upload og udsend`,Help:`Hjælp`,"Reload Player":`Genindlæs afspiller`,"Copy Bundled CLI Path":`Kopiér stien til medfølgende CLI`,"Developer tools":`Udviklerværktøjer`,Undo:`Fortryd`,Redo:`Gentag`,Cut:`Klip`,Paste:`Indsæt`,"Select all":`Vælg alle`,Status:`Status`,"Activity log":`Aktivitetslog`,Admin:`Administration`,"Playback controls":`Afspilningskontroller`,"Tracks and folders":`Numre og mapper`,"Original-language captions.":`Undertekster på originalsproget.`,"Skip to playback controls":`Spring til afspilningskontroller`,"Skip to playlist":`Spring til afspilningslisten`,"Skip to transcript and translated audio":`Spring til transskription og oversat lyd`};export{t as default};
1
+ import{n as e}from"./index-D-PZ4nGC.js";var t={...e,"No parties live right now.":`Ingen fester er live lige nu.`,"Could not refresh parties.":`Kunne ikke opdatere festerne.`,"Pick a server in the directory, or paste its link here.":`Vælg en server i oversigten, eller indsæt dens link her.`,"Interface language":`Grænsefladesprog`,Language:`Sprog`,"Translation language":`Oversættelsessprog`,"Original (auto-detect)":`Original (automatisk registrering)`,Play:`Afspil`,Pause:`Pause`,Stop:`Stop`,Next:`Næste`,Previous:`Forrige`,Select:`Vælg`,"Next/Prev":`Næste/Forrige`,Quit:`Afslut`,"Now Playing":`Afspilles nu`,"Spectrum Analyser":`Spektrumanalysator`,Playlist:`Afspilningsliste`,Files:`Filer`,"Open files":`Åbn filer`,"Open folder":`Åbn mappe`,"Add to library":`Føj til bibliotek`,"Replace playlist":`Erstat afspilningsliste`,Filter:`Filtrer`,Search:`Søg`,"Nothing loaded.":`Intet indlæst.`,"Empty.":`Tomt.`,"Loading…":`Indlæser…`,"Connecting…":`Forbinder…`,Connected:`Forbundet`,"Not connected":`Ikke forbundet`,"No source":`Ingen kilde`,Playing:`Afspiller`,Stopped:`Stoppet`,Volume:`Lydstyrke`,Seek:`Afspilningsposition`,"Full screen":`Fuld skærm`,Download:`Download`,Share:`Del`,Copy:`Kopiér`,"Copy link":`Kopiér link`,Copied:`Kopieret`,Save:`Gem`,Cancel:`Annuller`,Close:`Luk`,Remove:`Fjern`,Delete:`Slet`,Edit:`Rediger`,Add:`Tilføj`,Send:`Send`,Retry:`Prøv igen`,Back:`Tilbage`,"Next page":`Næste side`,"Previous page":`Forrige side`,"Show more":`Vis flere`,Panels:`Paneler`,"Reset layout":`Nulstil layout`,Show:`Vis`,Hide:`Skjul`,Collapse:`Fold sammen`,Expand:`Fold ud`,"Move up":`Flyt op`,"Move down":`Flyt ned`,Install:`Installer`,"TV mode":`TV-tilstand`,On:`Til`,Off:`Fra`,Account:`Konto`,"Sign in":`Log ind`,"Sign out":`Log ud`,"Create account":`Opret konto`,Email:`E-mail`,Password:`Adgangskode`,"Forgot password?":`Glemt adgangskode?`,"Welcome back":`Velkommen tilbage`,Profile:`Profil`,Handle:`Brugernavn`,"Your servers":`Dine servere`,Server:`Server`,Connect:`Forbind`,Disconnect:`Afbryd`,Directory:`Oversigt`,Favourites:`Favoritter`,Follow:`Følg`,Unfollow:`Følg ikke længere`,Notifications:`Notifikationer`,"Phone number":`Telefonnummer`,"Save number":`Gem nummer`,Transcript:`Transskription`,Captions:`Undertekster`,"Translate audio":`Oversæt lyd`,"Audio language":`Lydsprog`,"Audio options":`Lydindstillinger`,"Keep background sound":`Bevar baggrundslyd`,"Background level":`Baggrundslydstyrke`,"Speaker voices":`Talernes stemmer`,"Any voice":`Vilkårlig stemme`,"Male voice":`Mandlig stemme`,"Female voice":`Kvindelig stemme`,"Original audio.":`Original lyd.`,"Buy translated audio":`Køb oversat lyd`,"Audio credit":`Lydsaldo`,"Buy pass":`Køb adgang`,Pass:`Adgang`,"Pay with":`Betal med`,"Check payment":`Kontrollér betaling`,"Open checkout":`Gå til betaling`,"Free sessions":`Gratis sessioner`,"Usage rates and expiry":`Priser og udløb`,Catalogs:`Kataloger`,"Add catalog":`Tilføj katalog`,"Catalog name":`Katalognavn`,Link:`Link`,"Play link":`Afspil link`,"Go live":`Gå live`,Publish:`Udgiv`,Listen:`Lyt`,"Live now":`Live nu`,Upcoming:`Kommende`,Parties:`Seerfester`,"Join party":`Deltag i festen`,"Invite code":`Invitationskode`,Invite:`Inviter`,Chat:`Chat`,"Chat message":`Chatbesked`,People:`Personer`,Speaker:`Taler`,Speakers:`Talere`,Listener:`Lytter`,Moderator:`Moderator`,"Raise hand":`Ræk hånden op`,"Hand raises":`Oprakte hænder`,Questions:`Spørgsmål`,Resources:`Materialer`,Replay:`Genudsendelse`,Schedule:`Program`,Event:`Begivenhed`,Host:`Vært`,About:`Om`,Stage:`Scene`,Title:`Titel`,Description:`Beskrivelse`,Topic:`Emne`,Public:`Offentlig`,"Anyone with the link":`Alle med linket`,"Invited people":`Inviterede personer`,"Who can join":`Hvem kan deltage`,"Date and time":`Dato og klokkeslæt`,"Expected length":`Forventet varighed`,Repeat:`Gentag`,"Does not repeat":`Gentages ikke`,"Every day":`Hver dag`,"Every week":`Hver uge`,Homepage:`Hjemmeside`,"Host name":`Værtens navn`,"Profile photo URL":`URL til profilbillede`,"Broadcast link":`Udsendelseslink`,"Edit event":`Rediger begivenhed`,"Save changes":`Gem ændringer`,"Create live":`Opret livesending`,"Schedule class":`Planlæg undervisning`,"Start class":`Start undervisning`,"End event":`Afslut begivenhed`,"End session":`Afslut session`,"Session ended":`Session afsluttet`,"Event ended":`Begivenhed afsluttet`,Cancelled:`Annulleret`,"Replay available":`Genudsendelse tilgængelig`,Join:`Deltag`,"Create invite":`Opret invitation`,"Invite as":`Inviter som`,"Watch now":`Se nu`,"Start a room":`Opret et rum`,"Recent replays":`Seneste genudsendelser`,"Your account":`Din konto`,"Write with AI":`Skriv med AI`,"Suggested title":`Foreslået titel`,"Suggested description":`Foreslået beskrivelse`,"Suggested topic":`Foreslået emne`,"Use these details":`Brug disse oplysninger`,"Learn something live.":`Lær noget live.`,"Screen share":`Del skærm`,"Upload and broadcast":`Upload og udsend`,Help:`Hjælp`,"Reload Player":`Genindlæs afspiller`,"Copy Bundled CLI Path":`Kopiér stien til medfølgende CLI`,"Developer tools":`Udviklerværktøjer`,Undo:`Fortryd`,Redo:`Gentag`,Cut:`Klip`,Paste:`Indsæt`,"Select all":`Vælg alle`,Status:`Status`,"Activity log":`Aktivitetslog`,Admin:`Administration`,"Playback controls":`Afspilningskontroller`,"Tracks and folders":`Numre og mapper`,"Original-language captions.":`Undertekster på originalsproget.`,"Skip to playback controls":`Spring til afspilningskontroller`,"Skip to playlist":`Spring til afspilningslisten`,"Skip to transcript and translated audio":`Spring til transskription og oversat lyd`};export{t as default};