/**
 * Server mode: nixamp keeps playing on this machine and hands out a remote.
 *
 *   nixamp serve ~/Music --port 4321
 *
 * The same decode that feeds the speakers feeds the analyser, exactly as in the
 * terminal app; the HTTP layer only reads the state it produces and writes the
 * commands a remote sends. State is pushed over Server-Sent Events rather than
 * a WebSocket because SSE is plain HTTP: no dependency, and it reconnects on
 * its own when the laptop running the remote goes to sleep.
 */
import { publishChoice, shouldRecord } from "./publish-choice.ts";
import { protocolArgs } from "./decoder-policy.ts";
import { createReadStream, existsSync, statSync } from "node:fs";
import { createServer as createHttpServer, type IncomingMessage, type Server, type ServerResponse } from "node:http";
import { createServer as createHttpsServer } from "node:https";
import { randomBytes } from "node:crypto";
import { hostname, networkInterfaces } from "node:os";
import { spawn, spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";
import { Readable } from "node:stream";
import { pipeline as pipeStream } from "node:stream/promises";
import { relative } from "node:path";
import { SharedTranslations, type SharedEvent } from "./shared-translation.ts";
import { TranslationPasses, coinpayKeyFrom, readCoinpayWebhook, verifyCoinpaySignature } from "./translation-passes.ts";
import { LiveVoice, LIVE_VOICE_LANGUAGES, LIVE_VOICE_MODEL, type VoiceRequest } from "./live-voice.ts";
import { Connections, type Kind } from "./connections.ts";
import {
  Broadcaster,
  DEFAULT_ENCODER,
  type Destination,
  type EncoderSettings,
  PRESETS,
  redact,
} from "./broadcast.ts";
import { Ingest, normaliseFormat } from "./ingest.ts";
import {
  BACKLOG_SECONDS, Channels, cleanId, generatedId, rememberChannels, rememberedChannels, rememberedNow,
  type Channel, type ChannelInfo, type PullResume, type RememberedChannel,
} from "./channels.ts";
import { RtmpListeners } from "./rtmp-in.ts";
import { Accounts, clearedCookie, sessionCookie, tokenFrom } from "./accounts.ts";
import { RESET_INVALID, RESET_MESSAGE, resendPasswordReset, type ResetMail } from "./password-reset.ts";
import { passwordResetPage } from "./password-reset-page.ts";
import { anonymousHandle, Handles } from "./handles.ts";
import { OpenDirs } from "./opendirs.ts";
import { Servers } from "./servers.ts";
import { AccountProfiles, hostCard, PHOTO_LIMIT } from "./profiles.ts";
import { NixampLinks, nixampExchange } from "./nixamp-link.ts";
import { handleNixampLinkApi, nixampLinkPath } from "./nixamp-link-api.ts";
import { DeviceGrants } from "./device.ts";
import { BAD_KEY_LIMIT, callerOf, Guard, SIGN_IN_LIMIT } from "./guard.ts";
import {
  deviceDonePage,
  devicePage,
  exchangeCode,
  providersFrom,
  safeRedirect,
  signInReturnHosts,
  signInFailedPage,
  SignIn,
} from "./oauth.ts";
import { AuthorizationServer, BACKTOSCHOOL_CLIENT, SCOPE_NAMES, clientsFrom } from "./oauth-server.ts";
import { handleOAuthApi, oauthApiPath } from "./oauth-api.ts";
import { WatchParties } from "./watch-party.ts";
import { needsAdmin, needsMember, Owner } from "./owner.ts";
import { createHash as sha } from "node:crypto";
import { playJingle } from "./jingle.ts";
import { stateDir } from "./daemon.ts";
import { readSession } from "./session.ts";
import { Directory, ENDED_TTL_MS, parseAnnouncement, type LineupEntry, type Listing } from "./directory.ts";
import { ArtCache, coverArtOf, stillFrom } from "./art.ts";
import { PartyLine, telnyxSms } from "./partyline.ts";
import { HlsPackagers, segmentType, withKey } from "./hls.ts";
import { refusedDestination } from "./destinations.ts";
import { guideEntries, handleStations, isStationListenPath } from "./station-routes.ts";
import { handleLibrary } from "./library-routes.ts";
import { LibraryIndex, probeItem, trackOf, type Probed } from "./library-index.ts";
import { ListenerStore } from "./library-user.ts";
import { Importer } from "./handoff.ts";
import { handleHandoffs } from "./handoff-routes.ts";
import { handleAiRoutes, isPublicSharePath } from "./ai-routes.ts";
import { AiLabel, VeoUnavailable } from "./ai-label.ts";
import { buyLink, handleLabelRoutes, isPublicLabelPath } from "./label-routes.ts";
import { GrantVerifier, MusicStore, PRICES } from "./music-store.ts";
import { handleMusicRoutes, isMusicPath, MusicHub, musicPageFor, serverId } from "./music-hub.ts";
import { fileLedger, pgLedger, VeoBudgetError, VeoStudio } from "./veo-studio.ts";
import { KnownServers } from "./known-servers.ts";
import { handleStoreRoutes, isStorePath } from "./store-routes.ts";
import { MusicLicenses } from "./music-license.ts";
import { handleLicenseRoutes, isLicensePath } from "./license-routes.ts";
import { AiRadio } from "./ai-radio.ts";
import { ListenerShares } from "./listener-shares.ts";
import { HolderStore, TagReader, factsFor } from "./track-facts.ts";
import { SUPPORT_CSP, SUPPORT_PAGE, Support, SupportError, SupportSubscriptions, supportHoldersFrom } from "./support.ts";
import { LyricsFinder } from "./lyrics.ts";
import { LyricsPublisher } from "./lyrics-publish.ts";
import { aiKey } from "./ai-keys.ts";
import { STATION_APP_CSP, STATION_PAGE, STATION_PAGE_CSP, STATION_PAGE_HTML, WATCH_APP_CSP } from "./station-page.ts";
import { handleRoomProxy, handleRooms, isRoomPath, ROOM_PAGE, roomAddress, RoomProbe, roomEntries, roomSlugOf, roomSlugs, RoomStore, watchBase } from "./guide-rooms.ts";
import { handleVods, VodProbe, vodEntries, VodStore, vodView } from "./guide-vods.ts";
import { withPitFooter } from "./pit-footer.ts";
import { handleFollowPage } from "./follow-page.ts";
import {
  asRadioPage, buildCatalogue, isRadioPath, NAV_STYLE, RADIO_ALBUM, RADIO_ARTIST, RADIO_CSP, RADIO_STATION, RADIO_SUB, RADIO_BUY, radioBuyLink, radioBuyQuery, radioNav, radioPeers, radioRewriter,
  radioStations, renderAlbum, stationImage, renderArtist, renderDirectory, renderMissing, rewriteResponse, type PeerRadio, type RadioCatalogue,
} from "./radio-pages.ts";
import { adCsp, guideAds, guideCsp, withPageAd, guideHosts, guidePeers, peerBase, PeerGuides, renderGuide, type GuideEntry } from "./station-guide.ts";
import { StationRunner } from "./station-runner.ts";
import { StationError, StationStore, channelOf, stationOfChannel, type Station } from "./stations.ts";
import { STATION_CONTROL_LIMIT, StationUsage, limitsFromEnv, type Limits } from "./station-metering.ts";
import { StationBreaks } from "./station-breaks.ts";
import { DEFAULT_AUTO, syncAutoStations, type AutoOptions } from "./auto-stations.ts";
import { CompressionService } from "./compression/service.ts";
import { handleChannelCompression, handleCompressionApi, handleStaticRelay, type RouteContext } from "./compression/routes.ts";
import { DEFAULT_SITE as NICHEDB, Enricher, type EnrichKind, FIXTURE_TTL_MS } from "./enrich.ts";
import {
  contentTypeFor, downloadArgs, fileNameFor, inputArgsFor, linkChannelId, mergeDownloadArgs, playableLink, resolveLink,
  saveFormat,
  type ResolvedLink,
} from "./links.ts";
import { CALL_IN_NUMBER, OPT_IN_PATH, optInPage } from "./optin.ts";
import pg from "pg";
import { Follows, phoneFrom } from "./follows.ts";
import { Favorites, favoriteUrl } from "./favorites.ts";
import { SettingsSync, SETTINGS_BODY_LIMIT } from "./settings-sync.ts";
import { Catalogs, shownCatalog, shownEntry } from "./catalogs.ts";
import { Porkbun, isIPv4, isIPv6, type DnsZone } from "./dns.ts";
import { NameError, Names } from "./names.ts";
import { AcmeIssuer, Certs } from "./certs.ts";
import { claimName, fetchCert, humanizeSource, labelFor, readCertFiles, writeCertFiles } from "./naming.ts";
import { forbiddenLibrary, readConfig, readLibrary, stationIdleSeconds, writeConfig } from "./library.ts";
import { createThrottle, presentedCredential, type Throttle } from "@profullstack/throttle";
import { Durable } from "./durable.ts";
import { notifyAll, resendEmail, webPush, type Notification } from "./notify.ts";
import { vapidKeysFromEnv, vapidPublicKeyResponse, type VapidKeys } from "@profullstack/notifications/server";
import { inviteSubject, inviteText, isEmail, isPhone, watchLink } from "./invite.ts";
import { EventWriter } from "./event-writer.ts";
import { handleLiveApi } from "./live-api.ts";
import { LiveEvents, eventStructuredData, isTicketed, type LiveEvent } from "./live-events.ts";
import { Tickets, needsTicket, ticketFrom, ticketsFromEnv } from "./tickets.ts";
import { ListenPassVerifier, PASS_PARAM, checkListenPass, listenPassSecret, mintListenPass } from "./listen-pass.ts";
import { Layouts } from "./layouts.ts";
import { Rooms } from "./rooms.ts";
import { Trollbox, TrollboxError, fallbackHandle, roomFor } from "./trollbox.ts";
import { MAX_BYTES as SPEECH_BYTES, NATIVE_REVISION, Speech, SpeechError, isWav, languageOf } from "./speech.ts";
import { Captions } from "./captions.ts";
import { LANGUAGES, Translator } from "./translate.ts";
import { StoredTranslations } from "./translate-jobs.ts";
import {
  Transcripts, fileFingerprint, formatOf, idFrom, languageCode, linesFrom, mediaOfLive, mediaOfUrl, toSrt, toText, toVtt, transcriptIdOf, wire,
} from "./transcripts.ts";
import { handleMessage as mcpMessage } from "./mcp.ts";
import { PUBLIC_TOOLS, callPublicTool, isPublicTool } from "./directory-mcp.ts";
import { Media, factsFromRequest, holderFrom, mediaId, openFileListing, openFileOf, type MediaRecord } from "./media.ts";
import { mediaPage, type PageTranscript } from "./media-page.ts";
import { describeFile, refreshChanged } from "./media-local.ts";
import { WATCH_EVERY_MS, remember, watchOnce } from "./media-index.ts";
import { Outro } from "./outro.ts";
import { Profiles, Voices, spokenLine, spokenVoiceFor } from "./voices.ts";
import { confirm, DEFAULT_DIRECTORY, Publisher } from "./publish.ts";
import {
  applyRemoteConfig,
  createPaywall,
  FREE_LISTENERS,
  type PaywallConfig,
  paywallFromEnv,
} from "./paywall.ts";
import { isRemote, isTransportStream, playsInBrowser, sourceLabel } from "./sources.ts";
import { codecsOf, probeAsync, transportInputArgs, videoArgs, type Codecs } from "./audio.ts";
import {
  allowedForListening,
  elevate,
  firewallInUse,
  certifiable,
  keyCookie,
  keyInPath,
  rememberedKeys,
  keyFrom,
  keysMatch,
  lookupPublicIp,
  newKey,
  portCommands,
  reachableAddresses,
  scopeOf,
  shareLink,
  audioLink,
  KEY_QUERY,
} from "./share.ts";
import { extname, join, normalize, resolve, sep } from "node:path";
import { fileURLToPath } from "node:url";
import {
  detectTools, peaks, RATE, Stream, toMono,
  type Tools, type Track,
} from "./audio.ts";
import { Analyser, bandEdges, bands, decay } from "./fft.ts";
import { loadSource, loadTagged, readRemoteIndex } from "./playlist.ts";
import { nextAdvert } from "./ads.ts";
import { remoteSubject } from "./remote-subject.ts";
import {
  emptySnapshot, parseCommand,
  type Command, type RemoteTrack, type Snapshot,
} from "./protocol.ts";

const FFT_SIZE = 2048;
export const SERVE_BAND_COUNT = 24;
export const DEFAULT_PORT = 4321;

export interface ServeOptions {
  root: string;
  port: number;
  host: string;
  /** Directory of built PWA files to serve at `/`, when there is one. */
  web: string | null;
  /** Stream the library's bytes to remotes. Off keeps the audio on this box. */
  media: boolean;
  /**
   * Require the key from the share link. Off serves to anyone who can reach the
   * port, which is what the public deployment wants and no private one does.
   */
  key: boolean;
  /** Mint a new share key rather than reusing the one this port had. */
  newKey: boolean;
  /** Start without the noise it makes when it wakes up. */
  noJingle: boolean;
  /** Do not ask nixamp.com for a name and a certificate, even when signed in. */
  noName: boolean;
  /**
   * Ask the local firewall to let the port through, and put it back on the way
   * out. Off by default because it changes the machine, not just this process.
   */
  openPort: boolean;
  /**
   * Print one JSON line once listening. `nixamp daemon start` reads it rather
   * than sleeping and hoping, so a daemon that failed to bind is reported as
   * failed instead of started.
   */
  announce: boolean;
  /** Host the public directory. Only the deployment behind nixamp.com does. */
  directory: boolean;
  /**
   * List this stream at nixamp.com/directory. "ask" prompts, and is the
   * default: publishing an address without being asked is not something a
   * player gets to decide for you.
   */
  publish: "ask" | "yes" | "no";
  /** What to call it in the list. Defaults to this machine's hostname. */
  name: string;
  /**
   * The address this server is reachable at from outside, when that is not one
   * of its own interfaces: a tunnel, a reverse proxy, a forwarded port.
   *
   * Without it a machine behind NAT has nothing to publish -- every address it
   * can see is a 192.168 one that is no use to anybody else -- so the directory
   * listing is skipped and the printed links only work inside the house.
   */
  publicUrl: string;
  /**
   * A certificate and its key, to serve https rather than http.
   *
   * Needed by anybody whose nixamp is opened from a page that is itself https:
   * a browser refuses every request from an https page to an http one --
   * fetch, event stream and media alike -- and no header on either side lifts
   * that. It is deliberately not required: a nixamp on 192.168.1.5 cannot have
   * a certificate for that address, and forcing one would put a browser
   * warning in front of everybody at home to fix a problem they do not have.
   */
  tlsCert: string;
  tlsKey: string;
  /**
   * Ask an outside service what this machine's public address is, when no
   * interface holds one and none was given. Behind NAT that is the only way to
   * learn it, and it is one short request at startup.
   */
  lookup: boolean;
  /**
   * Charge for listening once the stream is busy. Off unless asked for, and
   * useless without somewhere to pay: see NIXAMP_PAY_TO.
   */
  x402: boolean;
  /** The account id that may administer this server, if not the signed-in one. */
  owner: string;
  /** Accept a live stream from a phone or a desktop, over HTTP. */
  ingest: boolean;
  /**
   * Also listen for RTMP publishers on this port, which is what OBS, Larix and
   * anything else native speaks. 0 means do not.
   */
  rtmpIn: number;
  /**
   * How many RTMP publishers may be live at once. ffmpeg's listener serves one
   * connection per process, so this is a port and a process each: 1935, 1936,
   * and so on. HTTP publishers are not limited by this.
   */
  rtmpStreams: number;
  /**
   * RTMP destinations, as `name=rtmp://host/app/key` or `youtube=key` for one
   * of the presets. Repeatable.
   */
  rtmp: string[];
}

/**
 * Read DATABASE_URL the way libpq does.
 *
 * node-postgres reads `sslmode=require` as verify-full, so a cluster that
 * signs its own certificate (dev2's shared Postgres) refuses every query with
 * "self signed certificate" while the process itself stays up and healthy:
 * pages serve, accounts and follows and events all fail. libpq, and every
 * other client, read `require` as "encrypt, do not verify", and pg honours
 * that reading when the URL carries `uselibpqcompat=true`. Railway's internal
 * URL had no sslmode at all, which is why this never came up there. Only
 * `require` and `prefer` are rewritten: `no-verify` is pg's own spelling and
 * has no libpq meaning, and `verify-*` asks for exactly what pg does anyway.
 */
export function libpqDatabaseUrl(raw: string | undefined): string | undefined {
  if (!raw || !/[?&]sslmode=(require|prefer)\b/.test(raw) || /[?&]uselibpqcompat=/.test(raw)) return raw;
  return `${raw}${raw.includes("?") ? "&" : "?"}uselibpqcompat=true`;
}

/**
 * Flags are parsed by hand: three of them do not justify a dependency, and the
 * failure mode of a wrong `--port` should be a message rather than NaN.
 */
export function parseServeArgs(argv: string[]): ServeOptions {
  // A platform that hands out the port does it through PORT; a flag still wins.
  const fromEnv = Number(process.env.PORT);
  const options: ServeOptions = {
    // Empty, not ".": nothing about a server should depend on where it was
    // started from. The saved library fills it in, or the start refuses.
    root: "",
    port: Number.isInteger(fromEnv) && fromEnv > 0 && fromEnv <= 65535 ? fromEnv : DEFAULT_PORT,
    // Every interface, because a player nobody else can reach is not much of a
    // remote. The key in the link is what makes that safe; --no-key gives up
    // both at once, and --host pins it back to one address.
    host: "0.0.0.0",
    web: null,
    media: true,
    key: true,
    newKey: false,
    noJingle: false,
    noName: false,
    openPort: false,
    announce: false,
    directory: false,
    publish: "ask",
    name: "",
    publicUrl: process.env["NIXAMP_PUBLIC_URL"] ?? "",
    lookup: true,
    tlsCert: process.env["NIXAMP_TLS_CERT"] ?? "",
    tlsKey: process.env["NIXAMP_TLS_KEY"] ?? "",
    x402: false,
    owner: "",
    ingest: false,
    rtmpIn: 0,
    rtmpStreams: 3,
    rtmp: [],
  };
  let sawRoot = false;
  const bothOrNeither = (): void => {
    if (Boolean(options.tlsCert) !== Boolean(options.tlsKey)) {
      throw new Error("nixamp serve: --tls-cert and --tls-key go together");
    }
  };
  for (let i = 0; i < argv.length; i++) {
    const arg = argv[i] as string;
    const value = (): string => {
      const next = argv[i + 1];
      if (next === undefined) throw new Error(`nixamp serve: ${arg} needs a value`);
      i++;
      return next;
    };
    if (arg === "--port" || arg === "-p") {
      const port = Number(value());
      if (!Number.isInteger(port) || port < 0 || port > 65535) {
        throw new Error("nixamp serve: --port must be a port number");
      }
      options.port = port;
    } else if (arg === "--host" || arg === "-h") {
      options.host = value();
    } else if (arg === "--web") {
      options.web = value();
    } else if (arg === "--no-media") {
      options.media = false;
    } else if (arg === "--no-key") {
      options.key = false;
    } else if (arg === "--open-port") {
      options.openPort = true;
    } else if (arg === "--announce") {
      options.announce = true;
    } else if (arg === "--directory") {
      options.directory = true;
    } else if (arg === "--publish") {
      options.publish = "yes";
    } else if (arg === "--no-publish") {
      options.publish = "no";
    } else if (arg === "--public-url") {
      const given = value().trim();
      // A hostname on its own is the likely typo, and it fails much later --
      // as a directory listing nobody can open -- so it is refused here.
      if (!/^https?:\/\/[^\s/]+/i.test(given)) {
        throw new Error("nixamp serve: --public-url must be a URL, e.g. https://nixamp.example.com");
      }
      options.publicUrl = given.replace(/\/+$/, "");
    } else if (arg === "--tls-cert") {
      options.tlsCert = value();
    } else if (arg === "--tls-key") {
      options.tlsKey = value();
    } else if (arg === "--no-lookup") {
      options.lookup = false;
    } else if (arg === "--name") {
      options.name = value();
    } else if (arg === "--owner") {
      options.owner = value();
    } else if (arg === "--new-key") {
      options.newKey = true;
    } else if (arg === "--no-jingle") {
      options.noJingle = true;
    } else if (arg === "--no-name") {
      options.noName = true;
    } else if (arg === "--ingest") {
      options.ingest = true;
    } else if (arg === "--rtmp-streams") {
      const count = Number(value());
      if (!Number.isInteger(count) || count < 1 || count > 16) {
        throw new Error("nixamp serve: --rtmp-streams must be between 1 and 16");
      }
      options.rtmpStreams = count;
    } else if (arg === "--rtmp-in") {
      const port = Number(value());
      if (!Number.isInteger(port) || port < 1 || port > 65535) {
        throw new Error("nixamp serve: --rtmp-in must be a port number");
      }
      options.rtmpIn = port;
      // Listening for RTMP is accepting a live stream, so it implies --ingest.
      options.ingest = true;
    } else if (arg === "--rtmp") {
      options.rtmp.push(value());
    } else if (arg === "--x402") {
      options.x402 = true;
    } else if (arg === "--no-x402") {
      options.x402 = false;
    } else if (arg.startsWith("-")) {
      throw new Error(`nixamp serve: unknown option ${arg}`);
    } else if (!sawRoot) {
      options.root = arg;
      sawRoot = true;
    }
  }
  bothOrNeither();
  return options;
}

const TYPES: Record<string, string> = {
  ".html": "text/html; charset=utf-8",
  ".js": "text/javascript; charset=utf-8",
  ".mjs": "text/javascript; charset=utf-8",
  ".css": "text/css; charset=utf-8",
  ".json": "application/json; charset=utf-8",
  ".webmanifest": "application/manifest+json; charset=utf-8",
  // The installer, so `curl https://nixamp.com/install.sh` is readable rather
  // than a download prompt.
  ".sh": "text/x-shellscript; charset=utf-8",
  ".ps1": "text/plain; charset=utf-8",
  ".svg": "image/svg+xml",
  ".png": "image/png",
  ".ico": "image/x-icon",
  ".woff2": "font/woff2",
  ".mp3": "audio/mpeg",
  ".flac": "audio/flac",
  ".ogg": "audio/ogg",
  ".oga": "audio/ogg",
  ".opus": "audio/ogg",
  ".m4a": "audio/mp4",
  ".aac": "audio/aac",
  ".wav": "audio/wav",
  ".wma": "audio/x-ms-wma",
  ".aiff": "audio/aiff",
  ".aif": "audio/aiff",
  ".alac": "audio/mp4",
  ".mp4": "video/mp4",
  ".webm": "video/webm",
};

export function contentType(path: string): string {
  return TYPES[extname(path).toLowerCase()] ?? "application/octet-stream";
}

export interface ByteRange {
  start: number;
  end: number;
}

/**
 * `Range: bytes=0-` and friends. Anything malformed, unsatisfiable or
 * multi-range is a null, which the caller answers with the whole file —
 * the behaviour a browser expects when it cannot have the range it asked for.
 */
export function parseRange(header: string | undefined, size: number): ByteRange | null {
  if (!header || size <= 0) return null;
  const match = /^bytes=(\d*)-(\d*)$/.exec(header.trim());
  if (!match) return null;
  const [, rawStart = "", rawEnd = ""] = match;
  if (rawStart === "" && rawEnd === "") return null;
  let start: number;
  let end: number;
  if (rawStart === "") {
    // A suffix range: the last N bytes.
    const length = Number(rawEnd);
    if (!Number.isFinite(length) || length <= 0) return null;
    start = Math.max(0, size - length);
    end = size - 1;
  } else {
    start = Number(rawStart);
    end = rawEnd === "" ? size - 1 : Number(rawEnd);
  }
  if (!Number.isFinite(start) || !Number.isFinite(end)) return null;
  if (start > end || start >= size) return null;
  return { start, end: Math.min(end, size - 1) };
}

/**
 * Resolve a URL path inside a directory, or null when it escapes.
 * `..` in a request path is the oldest bug in static file serving.
 */
export function safeJoin(rootDir: string, urlPath: string): string | null {
  let decoded: string;
  try {
    decoded = decodeURIComponent(urlPath);
  } catch {
    return null;
  }
  if (decoded.includes("\0")) return null;
  const base = resolve(rootDir);
  const full = resolve(base, "." + normalize(decoded.startsWith("/") ? decoded : `/${decoded}`));
  if (full !== base && !full.startsWith(base + sep)) return null;
  return full;
}

/**
 * A track and the source it arrived with.
 *
 * The library a server was started on has no group: it is simply what this
 * machine has. Anything added afterwards carries the name of the folder or
 * album it came from, which is what lets a client draw the two apart instead
 * of running them together.
 */
export type Loaded = Track & {
  group?: string;
  /** The folder it sits in, relative to what it was loaded from. */
  folder?: string;
  /**
   * Whether this has a picture, when the name could not say.
   *
   * A file on disk is named `film.mkv` and that is answer enough. A live
   * stream is `http://host/tipoffsport/KEY/301`, which says nothing at all --
   * so it was treated as audio, transcoded with `-vn`, and arrived as a
   * football match somebody could only listen to. Asked of ffprobe once, when
   * the source is added, rather than guessed from a URL that has no opinion.
   */
  picture?: boolean;
  /**
   * Whether a person named this, rather than a tagger.
   *
   * Tags are read in the background and merged in when they arrive, which is
   * right for a library and wrong for a channel: somebody called it "MLB
   * Network", ffprobe came back a moment later with "932", and the name they
   * chose vanished on its own.
   */
  named?: boolean;
};

/** What the HTTP layer needs from a player. Tests hand it a fake. */
export interface Engine {
  /** `withTracks` false leaves the library out, for a frame that is only motion. */
  snapshot(withTracks?: boolean): Snapshot;
  command(command: Command): void;
  subscribe(listener: (snapshot: Snapshot) => void): () => void;
  /** Absolute path of a track, or undefined when the index is not one. */
  trackPath(index: number): string | undefined;
  /**
   * Play something else instead of everything here.
   *
   * The big hammer, and no longer what adding a folder does: this is "point
   * this server somewhere else", which throws the library away on purpose.
   */
  replace(tracks: Track[], root: string): void;
  /** The library, arriving after the server was already listening. */
  fill(tracks: Track[], root: string): void;
  /**
   * This track turned out to have a picture after all.
   *
   * Whether a track is a film is worked out when it is added, and for an
   * address with no extension that means asking ffprobe. A track added before
   * that was asked -- or by an older nixamp -- keeps the wrong answer forever,
   * and every remote goes on putting a television channel into an audio
   * element. Streaming it is the moment the truth is known for certain.
   */
  sawPicture(index: number): void;
  /**
   * Play something as well as everything here.
   *
   * What somebody means by putting a folder in a box: the album shows up at
   * the bottom of the playlist under its own name, and the music that was
   * already there is still there. Answers how many tracks were new.
   *
   * `called` is what a person named it, which beats whatever the address ends
   * in -- a channel is "MLB Network", not "932".
   */
  add(tracks: Track[], from: string, called?: string): number;
  /**
   * Take an added source back out again, by the name `add` gave it.
   *
   * Nothing that came with the library can be dropped this way; the library is
   * what the server is, and there is a command line for changing that.
   */
  drop(group: string): number;
  /** Every added source, in the order they were added. */
  groups(): string[];
  /**
   * The same tracks, now with their tags.
   *
   * Startup lists filenames and begins serving immediately, because an ffprobe
   * per file over a real library takes minutes; the tags arrive afterwards and
   * land here. Unlike `replace` this must not disturb anything -- whoever is
   * listening keeps listening, and the only visible change is that the titles
   * fill in.
   */
  retag(tracks: Track[], root: string): void;
  stop(): void;
}

export function toRemoteTracks(tracks: Loaded[]): RemoteTrack[] {
  return tracks.map((t) => ({
    title: t.title,
    artist: t.artist,
    album: t.album,
    duration: t.duration,
    // Said out loud, because a remote cannot see the path and had been sending
    // every track to the audio element -- a film's soundtrack over a blank
    // panel, which is exactly what it looked like.
    // The name when it says something, what ffprobe found when it does not.
    ...(t.picture ?? hasPicture(t.path) ? { video: true } : {}),
    // Only for what was added; the library's own tracks say nothing, which is
    // how a client knows they are the library.
    ...(t.group ? { group: t.group } : {}),
    ...(t.folder ? { folder: t.folder } : {}),
    // Said plainly rather than guessed at from how it was loaded: this is
    // what puts a channel in the live list and a film in the library.
    ...(isRemote(t.path) ? { remote: true } : {}),
  }));
}

/** Video containers, as opposed to the songs that are most of a library. */
const PICTURE = new Set([
  ".mp4", ".mkv", ".avi", ".mov", ".m4v", ".webm", ".mpg", ".mpeg", ".wmv", ".flv",
  // Raw transport streams: a recording off a card, a receiver or an IPTV
  // dump, which is 1080p or 4K television and was arriving as its own
  // soundtrack because none of these names were on this list.
  ".m2ts", ".mts", ".m2t", ".trp", ".tp",
]);

export function hasPicture(path: string): boolean {
  const dot = path.lastIndexOf(".");
  if (dot > 0 && PICTURE.has(path.slice(dot).toLowerCase())) return true;
  // A `.ts` is a transport stream or a TypeScript file, and only its first
  // bytes know which. Asked of the file rather than of the name, and the
  // answer is remembered, because this is asked once per track per listing.
  return isTransportStream(path);
}

/**
 * Where a track sits, relative to the thing it was loaded from.
 *
 * A library is a shelf of albums and seasons, and a flat list of five thousand
 * files is one nobody can find anything in. This is what lets a player offer
 * the folders as folders.
 *
 * Relative and never absolute: the shape of somebody's library is what a
 * listener needs, and where it lives on their disk is not.
 */
export function folderOf(path: string, from: string): string {
  const strip = (value: string): string => value.replace(/\/+$/, "");
  const base = strip(from);
  if (base === "" || !path.startsWith(base + "/")) return "";
  const rest = path.slice(base.length + 1);
  const at = rest.lastIndexOf("/");
  if (at === -1) return "";
  const folder = rest.slice(0, at);
  // A URL's path is percent-encoded and a person reading a folder name is not.
  try {
    return isRemote(path) ? decodeURIComponent(folder) : folder;
  } catch {
    return folder;
  }
}

/**
 * Whether the name of a source tells us anything about what is inside it.
 *
 * A remote address with no extension -- an IPTV channel, a stream key, a
 * redirect -- is the case where it does not, and the only way to find out is
 * to look.
 */
export function nameSaysNothing(path: string): boolean {
  if (!isRemote(path)) return false;
  try {
    const last = new URL(path).pathname.split("/").pop() ?? "";
    return !last.includes(".");
  } catch {
    return false;
  }
}

/**
 * The headless player: the terminal app's engine without the terminal.
 * One ffmpeg decodes, ffplay makes the sound, and every sample is measured on
 * its way past so a remote can draw the same spectrum the TUI would.
 */
export class PlayerEngine implements Engine {
  private readonly listeners = new Set<(snapshot: Snapshot) => void>();
  private readonly analyser = new Analyser(FFT_SIZE, RATE);
  private readonly edges = bandEdges(SERVE_BAND_COUNT, RATE, FFT_SIZE);
  private readonly stream: Stream;
  private pending = new Float32Array(0);
  private revision = 0;
  /** Pushes are coalesced: the analyser fires far faster than a remote can draw. */
  private timer: ReturnType<typeof setInterval> | null = null;
  private dirty = false;

  private state = {
    index: 0,
    playing: false,
    position: 0,
    bars: new Array<number>(SERVE_BAND_COUNT).fill(0),
    levels: [0, 0] as [number, number],
    note: "",
  };

  constructor(
    private tracks: Loaded[],
    private root: string,
    tools: Tools,
    /** Frames a second pushed to remotes. */
    private readonly fps = 12,
  ) {
    if (tools.play === null) {
      this.state.note = "No audio output found (install ffplay) — analyser only.";
    }
    this.silent = tools.play === null;
    this.stream = new Stream(tools, {
      onSamples: (pcm) => this.consume(pcm),
      onEnd: (error) => {
        if (error) {
          this.state.note = error;
          this.state.playing = false;
          this.push();
          return;
        }
        this.command({ type: "next" });
      },
    });
  }

  private readonly silent: boolean;

  private consume(pcm: Float32Array): void {
    this.state.levels = peaks(pcm);
    this.state.position = this.stream.position;
    const mono = toMono(pcm);
    const joined = new Float32Array(this.pending.length + mono.length);
    joined.set(this.pending);
    joined.set(mono, this.pending.length);
    let at = 0;
    while (joined.length - at >= FFT_SIZE) {
      this.analyser.run(joined.subarray(at, at + FFT_SIZE));
      this.state.bars = decay(this.state.bars, bands(this.analyser.magnitudes, this.edges));
      at += FFT_SIZE;
    }
    this.pending = joined.subarray(at);
    this.dirty = true;
  }

  /**
   * The current state. `withTracks` carries the library, which is worth half a
   * megabyte on a real one and is only news when it has changed.
   */
  snapshot(withTracks = true): Snapshot {
    return {
      revision: this.revision,
      ...(withTracks ? { tracks: toRemoteTracks(this.tracks) } : {}),
      trackCount: this.tracks.length,
      index: this.state.index,
      playing: this.state.playing,
      position: this.state.position,
      bars: [...this.state.bars],
      levels: [this.state.levels[0], this.state.levels[1]],
      silent: this.silent,
      note: this.state.note,
      root: this.root,
    };
  }

  trackPath(index: number): string | undefined {
    return this.tracks[index]?.path;
  }

  command(command: Command): void {
    switch (command.type) {
      case "play":
        if (command.index !== undefined) this.state.index = this.clamp(command.index);
        this.start();
        break;
      case "toggle":
        if (this.state.playing) this.halt(); else this.start();
        break;
      case "stop":
        this.halt();
        break;
      case "next":
        this.step(1);
        break;
      case "prev":
        this.step(-1);
        break;
      case "select":
        this.state.index = this.clamp(command.index);
        if (this.state.playing) this.start();
        break;
    }
    this.push();
  }

  private clamp(index: number): number {
    if (this.tracks.length === 0) return 0;
    return Math.max(0, Math.min(this.tracks.length - 1, index));
  }

  private step(delta: number): void {
    if (this.tracks.length === 0) return;
    this.state.index = (this.state.index + delta + this.tracks.length) % this.tracks.length;
    if (this.state.playing) this.start(); else this.state.position = 0;
  }

  private start(): void {
    const track = this.tracks[this.state.index];
    if (!track) return;
    this.pending = new Float32Array(0);
    this.state.position = 0;
    this.state.playing = true;
    this.stream.start(track);
  }

  private halt(): void {
    this.stream.stop();
    this.state.playing = false;
    this.state.position = 0;
    this.state.bars = new Array<number>(SERVE_BAND_COUNT).fill(0);
    this.state.levels = [0, 0];
  }

  subscribe(listener: (snapshot: Snapshot) => void): () => void {
    this.listeners.add(listener);
    listener(this.snapshot());
    if (this.timer === null && this.listeners.size > 0) {
      this.timer = setInterval(() => {
        if (!this.dirty) return;
        this.dirty = false;
        this.push();
      }, Math.max(1, Math.round(1000 / this.fps)));
      this.timer.unref?.();
    }
    return () => {
      this.listeners.delete(listener);
      if (this.listeners.size === 0 && this.timer !== null) {
        clearInterval(this.timer);
        this.timer = null;
      }
    };
  }

  /**
   * Send the state to everyone watching.
   *
   * The library goes only when `listChanged` says it has, which is what turned
   * five megabytes a second into a few kilobytes: an analyser tick has nothing
   * to say about the track list, and it fires twelve times a second.
   */
  private push(listChanged = false): void {
    this.revision++;
    if (this.listeners.size === 0) return;
    const snapshot = this.snapshot(listChanged);
    for (const listener of this.listeners) listener(snapshot);
  }

  stop(): void {
    this.halt();
    if (this.timer !== null) {
      clearInterval(this.timer);
      this.timer = null;
    }
    this.listeners.clear();
  }

  replace(tracks: Track[], root: string): void {
    this.stop();
    this.tracks = tracks.map((track) => ({ ...track, folder: folderOf(track.path, root) }));
    this.root = root;
    this.state.index = 0;
    this.state.position = 0;
    this.state.note = "";
    this.push(true);
  }

  /**
   * Load something as well as what is already here.
   *
   * Adding a folder used to be `replace`, so pointing a server at an album on
   * the web threw away the music on its disk: the playlist you were looking at
   * turned into somebody else's twenty-eight tracks, and clicking your own
   * files played theirs. Nothing about playback changes here -- whatever was
   * playing keeps playing, at the same index, because the new tracks go on the
   * end.
   *
   * Paths already loaded are skipped, so adding the same album twice is not
   * two copies of it.
   */
  add(tracks: Track[], from: string, called = ""): number {
    // What somebody called it beats what the address happens to end in. A
    // channel at .../932 is "932" to a URL and "MLB Network" to a person, and
    // no transport stream reliably says which -- the one in front of us calls
    // itself "Service01".
    const group = called || sourceLabel(from);
    const known = new Set(this.tracks.map((track) => track.path));
    const fresh = tracks
      .filter((track) => !known.has(track.path))
      .map((track) => ({ ...track, group, folder: folderOf(track.path, from) }));
    if (fresh.length === 0) return 0;
    this.tracks = [...this.tracks, ...fresh];
    // The list itself changed, so it has to ride this frame; a count nobody
    // can index into is worse than no news at all.
    this.push(true);
    return fresh.length;
  }

  /**
   * Take an added source back out.
   *
   * The track that is playing is followed rather than an index: removing an
   * album from above the current track would otherwise slide the playlist out
   * from under a listener mid-song. If the playing track is itself in what is
   * being removed, playback stops -- there is nothing to keep playing.
   */
  drop(group: string): number {
    if (group === "") return 0;
    const playingPath = this.tracks[this.state.index]?.path;
    const kept = this.tracks.filter((track) => track.group !== group);
    const removed = this.tracks.length - kept.length;
    if (removed === 0) return 0;
    this.tracks = kept;
    const stillThere = kept.findIndex((track) => track.path === playingPath);
    if (stillThere === -1) {
      this.halt();
      this.state.index = this.clamp(this.state.index);
    } else {
      this.state.index = stillThere;
    }
    this.push(true);
    return removed;
  }

  sawPicture(index: number): void {
    const track = this.tracks[index];
    if (!track || track.picture === true) return;
    this.tracks = this.tracks.map((one, at) => (at === index ? { ...one, picture: true } : one));
    // The list changed in a way a client acts on -- which element it plays the
    // track in -- so it has to go out rather than wait for the next change.
    this.push(true);
  }

  groups(): string[] {
    const seen: string[] = [];
    for (const track of this.tracks) {
      if (track.group && !seen.includes(track.group)) seen.push(track.group);
    }
    return seen;
  }

  /**
   * The library, arriving after the server was already listening.
   *
   * Walking a directory is the slow part of starting -- eighty thousand files
   * under a home directory takes far longer than the fifteen seconds
   * `nixamp daemon start` waits for the server to say it is up, so starting a
   * daemon with no source given looked exactly like hanging and then failed
   * about a server that was working. The port opens first now and this puts
   * the library in behind it.
   *
   * Unlike `replace` it stops nothing and clears no listeners: whoever
   * connected in the first second is still connected, and simply sees the
   * playlist appear.
   */
  fill(tracks: Loaded[], root: string): void {
    // Something is already loaded, so this is a scan that finished after
    // somebody pointed the server elsewhere. Theirs wins.
    if (this.tracks.length > 0) return;
    this.tracks = tracks.map((track) => ({ ...track, folder: folderOf(track.path, root) }));
    this.root = root;
    this.state.note = tracks.length === 0 ? `No audio files under ${root}.` : "";
    this.push(true);
  }

  retag(tracks: Track[], root: string): void {
    // Matched by path rather than by position, because the list is no longer
    // required to be the one that was sent for tagging: somebody can add an
    // album while a library's tags are still being read, and an exact-shape
    // check would throw away every tag for it. Tags that describe tracks which
    // are no longer here simply match nothing, which is the same protection
    // the shape check was giving.
    void root;
    const byPath = new Map(tracks.map((track) => [track.path, track]));
    let changed = false;
    const merged = this.tracks.map((track) => {
      const tagged = byPath.get(track.path);
      if (!tagged || tagged === track) return track;
      changed = true;
      // The group is ours, not the tagger's: it knows what a track is called,
      // not which pile it is in.
      return {
        ...tagged,
        ...(track.group ? { group: track.group } : {}),
        ...(track.folder ? { folder: track.folder } : {}),
        // A name somebody chose outlives whatever the stream says about
        // itself. It called itself "932".
        ...(track.named ? { title: track.title, named: true } : {}),
      };
    });
    if (!changed) return;
    this.tracks = merged;
    // No stop, no index reset: the only thing that changes is what the titles
    // say, and every remote finds out because a snapshot goes out -- carrying
    // the list, since the titles are the whole point of this one.
    this.push(true);
  }
}

/**
 * The streams a server is carrying, gathered by name.
 *
 * A whole re-streamed folder is one entry rather than twenty: somebody
 * looking at what is on wants "that album from the web", not every track in
 * it. An entry names where to start, so clicking it plays.
 */
/**
 * A fetch-shaped Request for the throttle, built from the Node one.
 *
 * @profullstack/throttle is written against the web Request so it runs at an
 * edge; this server is Node's http. Only what the throttle reads is carried
 * across: method, URL and headers. The body is not, because metering is
 * decided before anybody reads it.
 */
export function requestFor(request: IncomingMessage, origin = "http://localhost"): Request {
  const headers = new Headers();
  for (const [name, value] of Object.entries(request.headers)) {
    if (typeof value === "string") headers.set(name, value);
    else if (Array.isArray(value)) headers.set(name, value.join(", "));
  }
  // The address, for a throttle that has no socket to ask.
  if (!headers.has("x-forwarded-for") && request.socket?.remoteAddress) {
    headers.set("x-forwarded-for", request.socket.remoteAddress);
  }
  return new Request(`${origin}${request.url ?? "/"}`, { method: request.method ?? "GET", headers });
}

/** Write a refusal the throttle produced back through the Node response. */
export async function answerWith(response: ServerResponse, refused: Response): Promise<void> {
  const headers: Record<string, string> = { ...CORS };
  refused.headers.forEach((value, name) => {
    headers[name] = value;
  });
  response.writeHead(refused.status, headers);
  response.end(Buffer.from(await refused.arrayBuffer()));
}

/**
 * How many channels a server will start on demand at once. Each is an ffmpeg,
 * and a catalog has thousands of entries; this is what keeps a room full of
 * curious people from becoming a room full of decoders.
 */
export const MAX_ON_DEMAND = 4;

/**
 * How often where each film has got to is written down. A restart lands
 * somewhere inside this, and REWIND covers the gap.
 */
export const REMEMBER_EVERY_MS = 15_000;

/**
 * What a member may have on the air at once, here, and what all members
 * together may. Each is a decoder on this machine. The owner is not counted:
 * it is their machine.
 */
export const MEMBER_LIVES_EACH = 2;
export const MEMBER_LIVES_TOTAL = 8;
/** How many times a minute one member may ask to go live. Three is a person. */
export const MEMBER_STARTS_PER_MINUTE = 3;

/**
 * Going live, metered per member. The session rides in the query from
 * another origin, which is where the throttle is told to look for who is
 * asking; the same session in a header or cookie counts as the same caller.
 * No gateway: over the limit is a 429 with Retry-After, not an offer.
 */
const memberThrottle = createThrottle({
  limit: MEMBER_STARTS_PER_MINUTE,
  // A credential buys a caller the larger budget by default -- six hundred a
  // minute, sized for a host of people behind one address. Here the
  // credential is the person, and the person gets the same three; the
  // ceiling is what one address may spend across every session it presents.
  credential: { limit: MEMBER_STARTS_PER_MINUTE, ceiling: MEMBER_STARTS_PER_MINUTE * 10 },
  credentialFrom: (request) =>
    new URL(request.url).searchParams.get("session") ?? presentedCredential(request.headers) ?? null,
});

/** Why a member may not put one more thing on the air here, or "" when they may. */
export function memberLiveRefusal(channels: Channels, account: string): string {
  const mine = channels.list().filter((one) => one.startedBy === account).length;
  if (mine >= MEMBER_LIVES_EACH) {
    return `you already have ${MEMBER_LIVES_EACH} on the air here; take one off first`;
  }
  const all = channels.list().filter((one) => one.startedBy).length;
  if (all >= MEMBER_LIVES_TOTAL) {
    return `this server is carrying ${MEMBER_LIVES_TOTAL} members' streams already; try again when one ends`;
  }
  return "";
}

/**
 * Probe a source and start carrying it as a channel of its own.
 *
 * Shared by the request that puts one on and the boot that puts remembered
 * ones back, so that both agree on what a source is encoded as. Null when
 * that channel id is already on.
 */
/** What is already known about a source, so it need not be asked, or asked twice. */
export interface KnownSource {
  kind?: "audio" | "video";
  codecs?: Codecs;
  position?: number;
  live?: boolean;
  /** A list of entries to play in turn, and which one was on. */
  playlist?: string[];
  playlistAt?: number;
  /** A picture and a line about it, from the site or the list that named it. */
  art?: string;
  about?: string;
  /** A station's rotation (see PullResume.rotation). */
  rotation?: PullResume["rotation"];
  /** kbps for an audio rendition; 192 when unset. */
  audioBitrate?: number;
}

/**
 * A station's files, chosen by track index (as /api/folders/live takes them)
 * or by a folder under the library; null when neither was given. Only files
 * on this machine and inside the library: a station never reaches past it.
 */
export function selectStationFiles(engine: Engine, body: { items?: unknown; folder?: unknown }): string[] | null {
  const root = engine.snapshot(false).root;
  if (Array.isArray(body.items)) {
    const indices = [...new Set(body.items.filter((one): one is number => Number.isInteger(one) && one >= 0))];
    const files = indices.map((index) => engine.trackPath(index)).filter((one): one is string => typeof one === "string" && !isRemote(one));
    if (indices.length !== body.items.length || files.length !== indices.length) {
      throw new StationError("unknown_items", "one or more of those tracks are not files in this library");
    }
    return files;
  }
  if (typeof body.folder === "string") {
    const folder = body.folder.replace(/\\/g, "/").replace(/^\/+|\/+$/g, "");
    if (folder.split("/").includes("..")) throw new StationError("invalid_folder", "a folder inside the library, please");
    const files: string[] = [];
    const count = engine.snapshot(false).trackCount;
    for (let index = 0; index < count; index++) {
      const file = engine.trackPath(index);
      if (!file || isRemote(file)) continue;
      const served = relative(root, file).replaceAll("\\", "/");
      if (served.startsWith("../")) continue;
      if (folder === "" || served.startsWith(`${folder}/`)) files.push(file);
    }
    if (files.length === 0) throw new StationError("empty_folder", "that folder has no files that play");
    return files;
  }
  return null;
}

const tracksByPath = new WeakMap<Engine, { revision: number; byPath: Map<string, { title: string; artist: string; album: string }> }>();

/** What the library knows about one file: its tags, for a now-playing line. */
export function trackFor(engine: Engine, file: string): { title: string; artist: string; album: string } | null {
  const revision = engine.snapshot(false).revision;
  let index = tracksByPath.get(engine);
  if (!index || index.revision !== revision) {
    const tracks = engine.snapshot().tracks ?? [];
    const byPath = new Map<string, { title: string; artist: string; album: string }>();
    tracks.forEach((track, at) => {
      const path = engine.trackPath(at);
      // Tags only: a title that is the file's own name is not a tag.
      const named = track.title && !looksLikeFileName(track.title) ? track.title : "";
      if (path) byPath.set(path, { title: named, artist: track.artist ?? "", album: track.album ?? "" });
    });
    index = { revision, byPath };
    tracksByPath.set(engine, index);
  }
  return index.byPath.get(file) ?? null;
}

/** How many times a source that answers nothing is asked, and how far apart. */
export const PROBE_TRIES = 3;
export const PROBE_RETRY_MS = 1500;

/**
 * Whether a channel may carry H.265 as it is.
 *
 * A channel has one encode and many viewers, so it has to be something they
 * can all play, and H.265 is not that: Safari and televisions decode it,
 * Chrome on a desktop mostly does not and shows nothing rather than saying
 * so. So an HEVC source is re-encoded by default -- to 1080p H.264, because a
 * 4K re-encode does not keep up with playback -- and an operator whose
 * audience is phones and televisions can say `NIXAMP_HEVC_CHANNELS=1` and
 * have the 4K copied through untouched. A single viewer asking for a file
 * over /api/media is a different matter: there the browser says for itself.
 */
export function hevcChannelsAllowed(env: NodeJS.ProcessEnv = process.env): boolean {
  return env["NIXAMP_HEVC_CHANNELS"] === "1";
}

export async function pullChannel(
  channels: Channels,
  ffprobe: string[],
  id: string,
  name: string,
  source: string,
  input: string[] = [],
  audio = "",
  known: KnownSource = {},
): Promise<Channel | null> {
  const tools = { ffmpeg: [], ffprobe, play: null };
  const empty = (c: Codecs): boolean => c.video === "" && c.audio === "";
  // A pair is probed as a pair: the picture's file has no sound in it, and
  // asked alone it would read as a silent film. The sound's codec comes from
  // the sound's file; the picture's, and the container, from the picture's.
  const probe = async (): Promise<Codecs> => {
    const [picture, sound] = await Promise.all([
      codecsOf(tools, source, input),
      audio ? codecsOf(tools, audio, input) : Promise.resolve(null),
    ]);
    return sound ? { ...picture, audio: sound.audio } : picture;
  };
  // Known already: a restart puts back what it wrote down and asks nobody.
  // Otherwise ask, and ask again when the answer is nothing: a film on an
  // IPTV panel allows one connection, and while the last ffmpeg's is still
  // being counted a probe gets an error page and no streams. Nothing, read
  // as "no picture", is how two films came back on the air as sound alone.
  let codecs: Codecs = known.codecs && !empty(known.codecs) ? known.codecs : { video: "", audio: "", container: "" };
  let assumed = false;
  if (empty(codecs)) {
    for (let attempt = 0; attempt < PROBE_TRIES; attempt++) {
      if (attempt > 0) await new Promise((r) => setTimeout(r, PROBE_RETRY_MS));
      codecs = await probe();
      if (!empty(codecs)) break;
    }
    assumed = empty(codecs);
  }
  // A source that would not say is carried as what it was last time, or as
  // video: a picture-less MP4 still plays, whereas a film as MP3 is a film
  // with no picture until somebody notices.
  const kind = codecs.video !== "" ? "video" : codecs.audio !== "" ? "audio" : (known.kind ?? "video");
  if (assumed) console.log(`  "${id}": the source would not say what it holds; carrying it as ${kind}.`);
  // A film has a length and a place to go back to; a live source has neither.
  // Listeners join a playlist live; its producer resumes the saved entry and position.
  const playlist = known.playlist && known.playlist.length > 0 ? known.playlist : null;
  const live = playlist ? true : (known.live ?? !((codecs.duration ?? 0) > 0));
  const encode = kind === "video"
    ? [
        // Which streams from which input, when there are two: the picture
        // from the first, the sound from the second. With one input ffmpeg
        // picks for itself, as it always did.
        ...(audio ? ["-map", "0:v:0", "-map", "1:a:0"] : []),
        ...videoArgs(codecs, 0, { allowHevc: hevcChannelsAllowed() }),
      ]
    // No picture in it, so none is invented: MP3 is the thing every browser
    // plays and the thing a listener can join halfway through.
    : ["-vn", "-c:a", "libmp3lame", "-b:a", `${known.audioBitrate ?? 192}k`, "-f", "mp3"];
  // A transport stream is read further into before it is decoded, and given
  // the timestamps a recording cut mid-stream does not carry. Ahead of the
  // caller's own input arguments, which are headers for the address itself.
  const opening = [...transportInputArgs(source, codecs.container), ...input];
  // A bare file is named by its file name until the file says better: an
  // MP3's tags name the episode and the show, and "Inspiring Founders" is a
  // card; "ep-0412-final.mp3" is not.
  const tagged = looksLikeFileName(name) && codecs.tags?.title ? codecs.tags.title : name;
  const channel = channels.pull(
    id, tagged, source, encode, kind, true, undefined, opening, kind === "video" ? audio : "",
    { live, position: known.position ?? 0, ...(playlist ? { playlist, playlistAt: known.playlistAt ?? 0 } : {}), ...(known.rotation ? { rotation: known.rotation } : {}) },
    // Known before the first dial: whether the source is a transport stream
    // decides whether it can be read here for a source-boundary relay.
    assumed ? undefined : codecs,
  );
  if (channel && !assumed) channel.info.codecs = codecs;
  if (channel) {
    // Its picture from wherever it came with one, and a line about it: what
    // was given, else the show and the album the file's own tags name.
    if (known.art && /^https?:\/\//i.test(known.art)) channel.info.art = known.art;
    const byTags = [codecs.tags?.artist, codecs.tags?.album].filter((one): one is string => Boolean(one)).join(" · ");
    const about = (known.about ?? byTags).trim().slice(0, 200);
    if (about !== "") channel.info.about = about;
    // A channel of sound put back from what an older nixamp wrote down was
    // never asked whether its file carries a sleeve. Asked now, once, after
    // it is on: reading the head of an MP3 on a CDN costs nothing and
    // opens no second connection a panel would count (that is a film's
    // trap, and a film has a picture of its own to take a frame of).
    if (kind === "audio" && !channel.info.art && known.codecs && codecs.cover === undefined) {
      void codecsOf(tools, source, input).then((later) => {
        if (channel.info.codecs && later.cover) channel.info.codecs.cover = true;
        if (!channel.info.about) {
          const line = [later.tags?.artist, later.tags?.album].filter((one): one is string => Boolean(one)).join(" · ");
          if (line !== "") channel.info.about = line;
        }
      });
    }
  }
  // What comes out, as opposed to what went in. An H.265 source copied
  // through stays H.265; one re-encoded arrives as H.264, and a packager
  // told otherwise would cut fMP4 segments for a stream that did not need
  // them.
  if (channel && kind === "video") {
    channel.info.emits = (playlist && playlist.length > 1 && playlist.every(one => !/^https?:\/\//i.test(one))) || encode.includes("libx264") ? "h264" : codecs.video || "h264";
  }
  return channel;
}

/** Whether a name is only a file's: something.mp3, with nothing said about it. */
export function looksLikeFileName(name: string): boolean {
  return /\.[a-z0-9]{2,5}$/i.test(name.trim());
}

/**
 * Where a channel's picture is, for the page and the directory.
 *
 * The site's own thumbnail as it is; else this server's art route, when
 * the channel has a picture to read one from -- a sleeve in the file, or a
 * moving picture to take a frame of. "" for a channel of sound with no
 * sleeve, which has no picture to be had.
 */
export function artOf(info: Pick<ChannelInfo, "id" | "art" | "kind" | "codecs">): string {
  if (info.art) return info.art;
  return info.codecs?.cover === true || info.kind === "video" ? `/api/channels/${encodeURIComponent(info.id)}/art` : "";
}

/**
 * The same, as a crawler reaches it: an absolute address on this server
 * with the listen key on it, since a preview is fetched by nobody who
 * holds the cookie. The listen link is what the directory publishes
 * already, so the key on the picture gives away nothing new.
 */
export function absoluteArtOf(listenLink: string, info: Pick<ChannelInfo, "id" | "art" | "kind" | "codecs">): string {
  const art = artOf(info);
  if (art === "" || /^https?:\/\//i.test(art)) return art;
  try {
    const link = new URL(listenLink);
    const key = /\/(?:view|admin)\/([^/?#]+)$/.exec(link.pathname)?.[1] ?? "";
    return `${link.origin}${art}${key ? `?${KEY_QUERY}=${encodeURIComponent(key)}` : ""}`;
  } catch {
    return "";
  }
}

/** A channel as the directory's lineup wants it: id, name, kind, picture, line. */
export function lineupEntry(listenLink: string, info: ChannelInfo): LineupEntry {
  return {
    id: info.id,
    name: info.name,
    ...(info.startedBy ? { startedBy: info.startedBy } : {}),
    kind: info.kind ?? "audio",
    art: absoluteArtOf(listenLink, info),
    about: info.about ?? "",
  };
}

/** What a link resolves to, kept so a download can be named without asking twice. */
const links = new Map<string, ResolvedLink>();

/**
 * A Netscape cookies file beside the state, if the operator has put one there.
 *
 * YouTube and Vimeo refuse a datacenter without a signed-in cookie; the
 * person who runs the server can export one from their browser and drop it
 * at ~/.local/state/nixamp/cookies.txt, and every link is asked for with it.
 */
export function cookiesFile(): string {
  const path = join(stateDir(), "cookies.txt");
  try {
    return statSync(path).isFile() ? path : "";
  } catch {
    return "";
  }
}

/**
 * What the page is told about a link it asked to play.
 *
 * Whether it is live is the channel's answer where there is one: yt-dlp
 * says nothing about an IPTV feed's liveness, and a feed told to the page
 * as a film got a Download button and a seek bar for a stream with no end.
 */
function shownLink(channelId: string, link: ResolvedLink, channel?: ChannelInfo) {
  const live = channel?.live ?? link.live;
  return {
    kind: "live",
    channel: channelId,
    name: channel?.name ?? link.title,
    live,
    // Whether there is a picture is the channel's answer where there is one:
    // a list is guessed at as video until its first entry has been probed,
    // and a station of podcasts opened a video element for nothing.
    video: channel?.kind ? channel.kind !== "audio" : link.video,
    duration: link.duration,
    extractor: link.extractor,
    // A live has no whole to keep; a bare file can be fetched by the browser itself.
    download: !live && link.extractor !== "direct",
    // How many things a list holds, for the page to say.
    entries: link.playlist?.length ?? 0,
    // Its picture and its line, for the meta line and the lock screen.
    art: channel ? artOf(channel) : link.thumbnail,
    about: channel?.about ?? link.about,
  };
}

export function liveOnes(engine: Engine): { name: string; at: number; tracks: number }[] {
  const tracks = engine.snapshot().tracks ?? [];
  const found = new Map<string, { name: string; at: number; tracks: number }>();
  tracks.forEach((track, at) => {
    if (track.remote !== true) return;
    const name = track.group || track.title;
    const already = found.get(name);
    if (already) already.tracks += 1;
    else found.set(name, { name, at, tracks: 1 });
  });
  return [...found.values()];
}

/** An engine with no library behind it, for the hosted PWA. */
export class EmptyEngine implements Engine {
  constructor(private readonly note = "No library on this server — open files, or point this remote at your own nixamp.") {}
  snapshot(): Snapshot {
    return { ...emptySnapshot(), note: this.note };
  }

  command(): void {}
  subscribe(listener: (snapshot: Snapshot) => void): () => void {
    listener(this.snapshot());
    return () => {};
  }
  trackPath(): undefined {
    return undefined;
  }
  replace(): void {}
  fill(): void {}
  sawPicture(): void {}
  add(): number {
    return 0;
  }
  drop(): number {
    return 0;
  }
  groups(): string[] {
    return [];
  }
  retag(): void {}
  stop(): void {}
}

const CORS: Record<string, string> = {
  // A remote is a browser on another device on the same network, so the
  // control API has to be reachable cross-origin. It exposes no filesystem
  // paths and takes six commands; binding to 127.0.0.1 is what keeps it shut.
  "access-control-allow-origin": "*",
  "access-control-allow-methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
  "access-control-allow-headers": "content-type, authorization",
  "access-control-max-age": "86400",
};

/**
 * How many nameless addresses are worth an ffprobe when a source is added.
 *
 * One is the ordinary case -- somebody pasting a channel -- and a directory
 * listing of thousands must not turn into thousands of probes for an answer
 * that only changes which element a browser uses.
 */
const PROBE_BY_HAND = 8;

/** /api/v1/<provider>/oauth/start and .../callback, the house callback shape. */
const OAUTH_ROUTE = /^\/api\/v1\/([a-z0-9-]+)\/oauth\/(start|callback)$/;

/**
 * Paths that are how somebody without a key gets one, so they answer before
 * the share-key check rather than behind it.
 */
export function isSignInPath(path: string): boolean {
  return (
    path.startsWith("/api/v1/auth/") ||
    // The account's own server list: nixamp.com's API, gated by the session
    // rather than by a share key it has nothing to do with.
    path === "/api/v1/servers" ||
    path.startsWith("/api/v1/servers/") ||
    path === "/api/v1/me/handle" ||
    // A listen pass is the session's to ask for and anybody's to check.
    path === "/api/v1/me/pass" ||
    path === "/api/v1/listen-pass/check" ||
    path === "/api/v1/me/profile" ||
    path.startsWith("/api/v1/me/profile/") ||
    // A stored photo is public: it is on every class the host runs.
    path.startsWith("/api/v1/profiles/") ||
    // An advert for a break. It has to answer without a key for the same
    // reason it exists: the listeners who get adverts are the ones who have
    // not signed in or paid.
    path === "/api/ads/next" ||
    // What this listener has paid for, asked before every break by the same
    // people and for the same reason, so it answers on the same terms.
    path === "/api/entitlements" ||
    // "Connect nixamp" on a site that is a client of nixamp.com.
    nixampLinkPath(path) ||
    // Public to read, so it must not be behind a share key either.
    path === "/api/v1/opendirs" ||
    path.startsWith("/api/v1/opendirs/") ||
    // nixamp as an authorization server: a client arriving here has no share
    // key and is not asking for one, and the metadata document is public by
    // the RFC that defines where it lives.
    oauthApiPath(path) ||
    OAUTH_ROUTE.test(path)
  );
}

function html(response: ServerResponse, code: number, body: string): void {
  response.writeHead(code, {
    "content-type": "text/html; charset=utf-8",
    "content-length": Buffer.byteLength(body),
    "cache-control": "no-store",
  });
  response.end(body);
}

function htmlText(value: string): string {
  return value
    .replaceAll("&", "&amp;")
    .replaceAll("<", "&lt;")
    .replaceAll(">", "&gt;")
    .replaceAll('"', "&quot;")
    .replaceAll("'", "&#039;");
}

/**
 * Whose site this shell belongs to.
 *
 * Read from the page rather than hard-coded, because one NixAmp serves several
 * branded clients and each brings its own head. og:site_name first, then the
 * part of the title before the dash or the colon ("nixamp: broadcast live
 * radio..." is nixamp), then the hostname.
 */
export function brandOf(shell: string, site: string): string {
  const named = /<meta\s+property="og:site_name"\s+content="([^"]+)"/i.exec(shell);
  if (named?.[1]) return named[1];
  const titled = /<title>([^<]*)<\/title>/i.exec(shell);
  const head = titled?.[1]?.split(/\s[—-]\s|:\s/)[0]?.trim();
  if (head) return head;
  try {
    return new URL(site).hostname.replace(/^www\./, "");
  } catch {
    return "NixAmp";
  }
}

function eventDocument(shell: string, event: Awaited<ReturnType<LiveEvents["get"]>>, site: string): string {
  if (!event) return shell;
  const brand = brandOf(shell, site);
  const title = `${event.title} — ${brand}`;
  const kindWord = event.kind === "concert" ? "Watch" : "Listen to";
  const description = event.description || `${kindWord} ${event.title} live on ${brand}.`;
  const canonical = `${site.replace(/\/$/, "")}/live/${encodeURIComponent(event.slug)}`;
  const structured = JSON.stringify(eventStructuredData(event, canonical)).replaceAll("<", "\\u003c");
  return shell
    .replace(/<title>.*?<\/title>/s, `<title>${htmlText(title)}</title>`)
    .replace(/<meta name="description" content="[^"]*"\s*\/>/, `<meta name="description" content="${htmlText(description)}" />`)
    .replace(/<meta property="og:title" content="[^"]*"\s*\/>/, `<meta property="og:title" content="${htmlText(event.title)}" />`)
    .replace(/<meta property="og:description" content="[^"]*"\s*\/>/, `<meta property="og:description" content="${htmlText(description)}" />`)
    .replace("</head>", `${event.visibility === "public" ? "" : '<meta name="robots" content="noindex,nofollow" />'}\n    <meta property="og:url" content="${htmlText(canonical)}" />\n    <link rel="canonical" href="${htmlText(canonical)}" />\n    <script type="application/ld+json">${structured}</script>\n  </head>`);
}

/**
 * What a join link is for, named by something this server actually knows.
 *
 * A link preview reads the page before any script runs, so the shell has to
 * say what is on the air. On the directory that is the listing the link
 * points at, and the channel on it if one is asked for; on a server it is
 * the server itself, or one of its channels. A name that only appears in the
 * query is not used: a page that titled itself with whatever the address said
 * would be a preview anyone could put words in.
 */
/** What a join link opens, as the shell's head says it: a name, where it is, and a picture and a line when it has them. */
export interface JoinSubject {
  title: string;
  where: string;
  /** An absolute picture address a crawler can fetch, or "". */
  image?: string;
  /** A line about it, or "". */
  about?: string;
  /** Whether the picture is a frame of a moving picture (wide) or a sleeve (square). */
  kind?: "audio" | "video";
}

export function joinSubject(
  url: URL,
  options: Pick<HandlerOptions, "directory" | "channels" | "serverName" | "live">,
): JoinSubject | null {
  const play = url.searchParams.get("play") ?? "";
  const wanted = play.startsWith("channel:") ? play.slice("channel:".length) : "";
  const link = url.searchParams.get("url") ?? "";
  const withArt = (subject: JoinSubject, entry: Pick<LineupEntry, "art" | "about" | "kind"> | undefined): JoinSubject => ({
    ...subject,
    ...(entry?.art ? { image: entry.art } : {}),
    ...(entry?.about ? { about: entry.about } : {}),
    ...(entry ? { kind: entry.kind } : {}),
  });
  if (options.directory) {
    if (link === "") return null;
    const listings = options.directory.list();
    const listing = listings.find((one) => one.url === link) ?? listings.find((one) => {
      try {
        return new URL(one.url).origin === new URL(link).origin;
      } catch {
        return false;
      }
    });
    if (!listing) return null;
    if (wanted !== "") {
      // A join link names a channel by id; the directory row names it by
      // name; the lineup knows both. An older server's listing has names
      // alone, and is matched on those as before.
      const entry = listing.lineup.find((one) => one.id === wanted) ?? listing.lineup.find((one) => one.name === wanted);
      if (entry) return withArt({ title: entry.name, where: listing.name }, entry);
      if (listing.channels.includes(wanted)) return { title: wanted, where: listing.name };
    }
    return { title: listing.name, where: "" };
  }
  // A link to somewhere else is that server's to name.
  if (link !== "") return null;
  const name = options.serverName ?? "";
  if (wanted !== "") {
    const channel = options.channels?.list().find((one) => one.id === wanted || one.name === wanted);
    if (!channel) return null;
    // Its picture as a crawler reaches it: absolute, with the listen key,
    // which the listing this server publishes carries anyway.
    const listen = options.live?.status().url ?? "";
    const entry = listen ? lineupEntry(listen, channel) : undefined;
    return withArt({ title: channel.name, where: name }, entry);
  }
  return name === "" ? null : { title: name, where: "" };
}

/** The shell, titled for what a join link opens, with its picture on the card. */
export function joinDocument(shell: string, subject: JoinSubject, site: string): string {
  const brand = brandOf(shell, site);
  const shellTitle = /<title>([^<]*)<\/title>/i.exec(shell)?.[1] ?? "";
  const title = `${subject.title} — ${brand}`;
  const where = subject.where === ""
    ? `${subject.title} is live on ${brand}. Tune in free, no account needed.`
    : `${subject.title} is live on ${subject.where}. Tune in free on ${brand}, no account needed.`;
  // What it is about comes first, then where it is: a card has two lines.
  const description = subject.about ? `${subject.about} — ${where}` : where;
  const image = subject.image ?? "";
  // A frame of a picture is wide and fills the card; a sleeve is square and
  // sits beside the words. Twitter has a card for each and picks neither
  // without being told; everything else reads og:image and decides itself.
  const card = image === "" ? "" : [
    `<meta property="og:image" content="${htmlText(image)}" />`,
    `<meta property="og:image:alt" content="${htmlText(subject.title)}" />`,
    `<meta name="twitter:card" content="${subject.kind === "video" ? "summary_large_image" : "summary"}" />`,
    `<meta name="twitter:title" content="${htmlText(subject.title)}" />`,
    `<meta name="twitter:description" content="${htmlText(description)}" />`,
    `<meta name="twitter:image" content="${htmlText(image)}" />`,
  ].map((line) => `${line}\n    `).join("");
  return shell
    .replace(/<title>.*?<\/title>/s, `<title>${htmlText(title)}</title>`)
    .replace(/<meta name="description" content="[^"]*"\s*\/>/, `<meta name="description" content="${htmlText(description)}" />`)
    .replace(/<meta property="og:title" content="[^"]*"\s*\/>/, `<meta property="og:title" content="${htmlText(subject.title)}" />`)
    .replace(/<meta property="og:description" content="[^"]*"\s*\/>/, `<meta property="og:description" content="${htmlText(description)}" />`)
    // What the tab is called once the page takes over, so it does not stack
    // the channel on a title that already names the channel.
    .replace("</head>", `${card}<meta name="nixamp-shell-title" content="${htmlText(shellTitle)}" />\n  </head>`);
}

function json(response: ServerResponse, code: number, body: unknown): void {
  if (code === 429) response.setHeader("retry-after", "60");
  const text = JSON.stringify(body);
  response.writeHead(code, {
    ...CORS,
    "content-type": "application/json; charset=utf-8",
    "content-length": Buffer.byteLength(text),
    "cache-control": "no-store",
  });
  response.end(text);
}

/** Stream PCM without waiting for a complete utterance, respecting browser backpressure. */
async function voiceResponse(response: ServerResponse, upstream: Response): Promise<void> {
  response.writeHead(upstream.status, {
    ...CORS, "content-type": upstream.headers.get("content-type") ?? "application/json",
    "cache-control": "no-store", "x-accel-buffering": "no",
  });
  if (!upstream.body) { response.end(); return; }
  await pipeStream(Readable.fromWeb(upstream.body as import("node:stream/web").ReadableStream), response);
}

async function readBody(request: IncomingMessage, limit = 64 * 1024): Promise<string> {
  const chunks: Buffer[] = [];
  let size = 0;
  for await (const chunk of request) {
    const buffer = chunk as Buffer;
    size += buffer.length;
    if (size > limit) throw new Error("body too large");
    chunks.push(buffer);
  }
  return Buffer.concat(chunks).toString("utf8");
}

/** The body as bytes: sound, not text. */
async function readBytes(request: IncomingMessage, limit: number): Promise<Uint8Array> {
  const chunks: Buffer[] = [];
  let size = 0;
  for await (const chunk of request) {
    const buffer = chunk as Buffer;
    size += buffer.length;
    if (size > limit) throw new SpeechError("that is too much sound", 413);
    chunks.push(buffer);
  }
  return new Uint8Array(Buffer.concat(chunks));
}

export interface HandlerOptions {
  /** The library as an index: sources, items, search. */
  library?: { index: LibraryIndex; probe: (path: string) => Promise<Probed>; retag: (tracks: Track[]) => void; listeners?: ListenerStore };
  /** Music handed over from BitTorrented. */
  importer?: Importer;
  /** This server's stations and what keeps them on the air. */
  stations?: {
    store: StationStore;
    runner: StationRunner;
    usage?: StationUsage;
    limits?: Limits;
    auto?: { sync: (input: Record<string, unknown>) => Promise<unknown>; setting: () => unknown };
    /** This server's public https address, once it is known. */
    publicBase?: () => string;
    /** Stations whose music is composed by AI. */
    ai?: AiRadio;
    /** AI artists and their albums, a genre station each, and the catalogue at /label. */
    label?: AiLabel;
    /** Tracks anybody may share, sorted onto the air by AI. */
    shares?: ListenerShares;
    /** What a listener may know about the track on the air. */
    facts?: (station: Station, file: string) => Promise<import("./track-facts.ts").TrackFacts>;
    /** The cover picture inside a file, for the now-playing card. */
    cover?: (file: string) => Promise<{ bytes: Buffer; type: string } | null>;
    /** The words of a file on the air, from its tags or nichedb. */
    lyrics?: (station: Station, file: string) => Promise<import("./lyrics.ts").Lyrics | null>;
    /** Our own songs' words, published back to nichedb. */
    lyricsPublisher?: import("./lyrics-publish.ts").LyricsPublisher;
    /** Copyright holders, matched to tracks by artist or station. */
    holders?: import("./track-facts.ts").HolderStore;
    /** Proxies (by address) whose X-Forwarded-For is believed, for per-sharer limits. */
    trustedProxies?: () => Set<string>;
    /** Other servers' public stations, for this guide to list beside its own. */
    peers?: PeerGuides;
    /** Live rooms members published to this guide, and how to ask a room's server what is on it. */
    rooms?: { store: RoomStore; probe: RoomProbe };
    /** Files members published to this guide to play on demand, and how to ask a file's server about it. */
    vods?: { store: VodStore; probe: VodProbe };
  };
  web: string | null;
  /** Branded clients on this same backend, keyed by an explicitly configured host. */
  webSites?: ReadonlyMap<string, import("./web-sites.ts").WebSite>;
  media: boolean;
  version: string;
  /** The key from the share link, or null to serve to anyone who can connect. */
  key?: string | null;
  /**
   * A second key that may listen but not drive. The public directory hands
   * this one out: a link that lets a stranger pause your music is not a link
   * you can publish.
   */
  listenKey?: string | null;
  /** How to run ffmpeg, for the sources a browser cannot play by itself. */
  ffmpeg?: string[];
  /** Where ffprobe is, for asking what is inside a file before re-encoding it. */
  ffprobe?: string[];
  /**
   * Whether this server can carry a channel at all: false where no ffmpeg
   * was found, as on the hosted directory, which then says so instead of
   * resolving a link, probing it, and blaming the site.
   */
  carries?: boolean;
  /** yt-dlp, which turns a pasted page into a media address. Null when there is none. */
  ytdlp?: string[] | null;
  /** Channels as HLS, for Safari on a phone, which plays a live stream no other way. */
  hls?: HlsPackagers;
  /** The pictures of channels already taken, so a card unfurled by five crawlers costs one ffmpeg. */
  art?: ArtCache;
  /** Captions for a channel: its sound, as lines, as they are heard. Needs ffmpeg and a sign-in. */
  captions?: Captions;
  /** Lossless relay compression, its policies, diagnostics and static representations. */
  compression?: CompressionService;
  /** What a name is -- a film, a channel, a fixture -- asked of nichedb.dev and remembered. */
  enricher?: Enricher;
  /** A Netscape cookies file for sites that want a signed-in browser, when there is one. */
  cookies?: string;
  /** Who is listening, for the admin view. */
  connections?: Connections;
  /**
   * How to turn a source into tracks, for re-streaming. Injected rather than
   * imported so the handler stays a plain function of a request.
   */
  load: (source: string) => Promise<Track[]>;
  /**
   * The same source, with its tags, read without holding the event loop. Called
   * after `load` and never awaited: the titles arrive into a player that is
   * already playing.
   */
  tag?: (source: string) => Promise<Track[]>;
  /**
   * The public directory, on the instance that hosts one. Only nixamp.com
   * passes this; a nixamp on your laptop is a publisher, not a registry.
   */
  directory?: Directory;
  /** Answers a request itself when listening has to be paid for. */
  paywall?: (request: IncomingMessage, response: ServerResponse, path: string) => Promise<boolean>;
  /** Live audio coming in from a phone or a desktop. */
  ingest?: Ingest;
  /** Several live streams at once, each with its own audience. */
  channels?: Channels;
  /** Write down the channels this server pulls, so a restart puts them back. */
  rememberChannels?: (list: RememberedChannel[]) => void;
  /** The m3u catalogs this server keeps, browsable by group. */
  catalogs?: Catalogs;
  /** Live audio going out to RTMP. */
  broadcaster?: Broadcaster;
  /** Where a broadcast should send, and what it should look like. */
  broadcast?: () => { destinations: Destination[]; settings: EncoderSettings };
  /** What this server calls itself, for the list of what is live on it. */
  serverName?: string;
  /**
   * The source this server was started on -- its own library.
   *
   * Replacing the playlist with a stream leaves no way back to it: the address
   * is a path on somebody else's machine, and a person looking at a player has
   * no reason to know it. Reported to an administrator so there can be a
   * button rather than a thing you have to remember and retype.
   *
   * Admin-only, because it is a filesystem path and a viewer has no business
   * with it.
   */
  homeSource?: string;
  /**
   * Going live: whether this server is listed, and how to change that.
   *
   * Listing used to be a question asked once at startup and never again, so a
   * server started with --no-publish had no listing, no phone code, and no
   * link to hand anybody -- and no way to change its mind short of stopping
   * and starting it. It is an action now, because that is what it is.
   */
  live?: {
    status: () => {
      live: boolean; code: string; name: string; url: string; possible: boolean;
      /** A phone code per live channel, by name, as the directory assigned them. */
      channelCodes?: Record<string, string>;
    };
    start: () => Promise<{ live: boolean; code: string; name: string; url: string; error?: string }>;
    stop: () => Promise<void>;
    /**
     * Tell the directory now rather than at the next heartbeat. A channel
     * that just went on the air should be in the list before the person who
     * put it there has looked, and a heartbeat is ninety seconds.
     */
    announce?: () => Promise<void>;
  };
  /**
   * Where OBS should point, one entry per stream this server will accept.
   *
   * There is deliberately no single link. ffmpeg's RTMP listener serves one
   * connection per process, so three people going live at once is three ports
   * and three URLs -- and a panel offering one address for all of them would
   * be offering an address that works exactly once.
   */
  publishUrls?: () => { id: string; url: string }[];
  /** Accounts, on the instance that keeps them. Only nixamp.com passes this. */
  accounts?: Accounts;
  /** Transactional password recovery mail, separate from follow notifications. */
  resetMail?: ResetMail;
  /** Providers to sign in with, and the terminals waiting to be connected. */
  signIn?: SignIn;
  /** The servers each account runs, on the instance that keeps accounts. */
  servers?: Servers;
  /** The name other people see, which is never the address they signed up with. */
  handles?: Handles;
  /** The account's card: a name, a homepage, a photo, a line. One table, read by every site here. */
  accountProfiles?: AccountProfiles;
  /** nixamp accounts connected to accounts here, where this site is a client of nixamp.com. */
  links?: NixampLinks;
  /** Open directories people have found, which anyone may read. */
  openDirs?: OpenDirs;
  /** True when this instance is reached over https, for the cookie's Secure. */
  secureCookies?: boolean;
  /** A certificate and key in PEM, when this server is to speak https itself. */
  tls?: { cert: string; key: string };
  /**
   * True when a proxy sits in front, so `x-forwarded-for` names the caller.
   * False everywhere else on purpose: the header is trivially forged, and
   * believing it from a direct caller hands them a fresh identity per request
   * and with it an unlimited number of password attempts.
   */
  behindProxy?: boolean;
  /** Who may administer this server. */
  owner?: Owner;
  /**
   * The dial-in party line, on the instance that answers the phone number.
   * Only nixamp.com passes this; a nixamp on a laptop has no number.
   */
  partyLine?: PartyLine;
  /**
   * Following broadcasters, and where to reach the people who do. Durable,
   * unlike everything else here, because the point of a follow is to outlive
   * the stream.
   */
  follows?: Follows;
  /** The servers an account hearted. nixamp.com only, like follows. */
  favorites?: Favorites;
  /** Settings sync, on the directory: a member's settings under revisions, reached by the same session. */
  settingsSync?: SettingsSync;
  /** Scheduled and live sessions, kept by NixAmp and shared by branded clients. */
  events?: LiveEvents;
  eventWriter?: EventWriter;
  onEventUpdated?: (event: LiveEvent, previous: LiveEvent) => void;
  /** Versioned panel layouts, including event and user overrides. */
  layouts?: Layouts;
  /** Persistent participation state that must not be coupled to live audio. */
  rooms?: Rooms;
  /** The trollbox: a chat per live room, kept at nixamp.com. */
  trollbox?: Trollbox;
  /** Speech to text: a line said out loud, heard here. Needs the optional model. */
  speech?: Speech;
  liveVoice?: LiveVoice;
  translationPasses?: TranslationPasses;
  /** Payments to copyright holders nixamp.com collects for (support.ts). */
  support?: Support;
  /** Monthly support, run by nixamp.com (support.ts). */
  supportSubscriptions?: SupportSubscriptions;
  /** Signing secret of nixamp's CoinPay business, for /api/v1/webhooks/coinpay. */
  coinpayWebhookSecret?: string;
  /** The AI label's songs for sale, nixamp.com only (music-store.ts). */
  musicStore?: MusicStore;
  /** Licences to use that music in games, apps and videos, nixamp.com only (music-license.ts). */
  musicLicenses?: MusicLicenses;
  /** nixamp.com/music: every server's label in one app (music-hub.ts). */
  music?: { hub: MusicHub; released: (request: IncomingMessage, body: Record<string, unknown>) => Promise<{ status: number; body: unknown }>; veo?: (request: IncomingMessage) => Promise<{ status: number; body: unknown; video?: Buffer }> };
  sharedTranslations?: SharedTranslations;
  /** Translation: texts in another language, by a model here. The same optional library. */
  translator?: Translator;
  /** The transcript store: what was heard, kept under the media's identity. Where the accounts are. */
  transcripts?: Transcripts;
  /** Stored transcripts translated, as jobs. */
  translations?: StoredTranslations;
  /** The record of every file nixamp has met, by its SHA-256: nixamp.com/hash/<id>. */
  records?: Media;
  /** nixamp.com's address, for the tools reached over /mcp to call. */
  site?: string;
  /** Other people's OpenProfiles, for the voice their lines are read in. */
  profiles?: Profiles;
  /** The voices lines are read in: ElevenLabs when Telnyx holds the key, Kokoro otherwise. */
  voices?: Voices;
  /** Tickets: a paid pass to one event's room. Absent means every show is free. */
  tickets?: Tickets;
  /**
   * nixamp as an OAuth 2.1 authorization server, so another site can act on
   * an account here. nixamp.com only: a nixamp on a laptop keeps no accounts
   * and so has nobody to authorize.
   */
  authServer?: AuthorizationServer;
  /** Watch parties bridged from a client site into nixamp rooms. */
  parties?: WatchParties;
  /**
   * How an invite is sent: by email, by text, and the site the watch link is
   * built on. nixamp.com only; a personal nixamp has no mail to send from.
   */
  invites?: {
    email?: (to: string, note: Notification) => Promise<boolean>;
    sms?: { send(to: string, text: string): Promise<boolean> };
    site: string;
  };
  /** Names under `<handle>.<zone>` for an account's servers. nixamp.com only. */
  names?: Names;
  /** One wildcard certificate per handle, issued and renewed here. nixamp.com only. */
  certs?: Certs;
  /** The zone the names live in, e.g. "nixamp.com". */
  dnsZone?: string;
  /**
   * The rate limit over everything, from @profullstack/throttle. Fetch-shaped,
   * so the handler builds a Request from the Node one and writes back the
   * Response it is refused with.
   */
  throttle?: Throttle;
  /**
   * The VAPID pair, read from the environment when the server starts. Only the
   * public half leaves: it is what a browser needs before it can subscribe.
   */
  vapidKeys?: VapidKeys | null;
}

/**
 * The whole HTTP surface, as a plain function of a request — so a test can
 * drive it with a real socket and no ffmpeg in sight.
 */
export function createHandler(engine: Engine, options: HandlerOptions) {
  // One per server, so the counters survive between requests and die with it.
  const guard = new Guard();

  const tracker = options.connections ?? new Connections();
  const started = Date.now();

  /** Count a request in, count its bytes, and close it out exactly once. */
  const watch = (
    request: IncomingMessage,
    response: ServerResponse,
    kind: Kind,
    track: string,
  ): void => {
    const { id } = tracker.open(request, kind, track);
    const write = response.write.bind(response);
    response.write = ((chunk: unknown, ...rest: unknown[]) => {
      if (typeof chunk === "string" || chunk instanceof Uint8Array) {
        tracker.add(id, typeof chunk === "string" ? Buffer.byteLength(chunk) : chunk.byteLength);
      }
      return (write as (...args: unknown[]) => boolean)(chunk, ...rest);
    }) as typeof response.write;
    // 'close' fires for a finished response and for a listener that walked
    // away, which are the same thing as far as "is it still going" goes.
    response.once("close", () => tracker.close(id));
  };

  // Signs listen passes where accounts live (nixamp.com); "" everywhere else.
  const listenPassKey = listenPassSecret();
  const listenPasses = new ListenPassVerifier({
    site: process.env["NIXAMP_SITE"] || "https://nixamp.com",
    ...(listenPassKey ? { secret: listenPassKey } : {}),
  });
  // The music store lives on nixamp.com; a server holding the label's music asks it about grants.
  const storeSite = (process.env["NIXAMP_SITE"] || "https://nixamp.com").replace(/\/+$/, "");
  const storeGrants = new GrantVerifier({ site: storeSite });
  // /radio's artists and albums, from the public stations' files and the label (radio-pages.ts), a minute at a time.
  let radioCache: { at: number; value: RadioCatalogue } | null = null;
  const radioCatalogue = (): RadioCatalogue => {
    if (radioCache && Date.now() - radioCache.at < 60_000) return radioCache.value;
    const label = options.stations?.label;
    const index = options.library?.index;
    const value = buildCatalogue({
      stations: radioStations(options.stations?.store.list() ?? []),
      ...(label ? { label: label.publicView(), songAt: (file: string) => label.songAt(file), audioFor: (file: string) => label.audioFor(file) } : {}),
      tags: (file) => {
        const item = index?.itemAt(file);
        if (!item) return null;
        const tags = { ...item, ...item.override };
        return { title: tags.title, artist: tags.artist || tags.albumArtist, album: tags.album, year: tags.year, track: tags.track, duration: item.duration, hasArt: item.hasArt };
      },
    });
    radioCache = { at: Date.now(), value };
    return value;
  };

  return async function handle(request: IncomingMessage, response: ServerResponse): Promise<void> {
    // A station's own paths under its /radio name (radio-pages.ts): asked of
    // the station's routes with its token, which never leaves this server,
    // and answered with the /radio addresses in place of the station's.
    if (options.stations && (request.url ?? "").startsWith("/radio/")) {
      const asked = new URL(request.url ?? "/", "http://localhost");
      const sub = RADIO_SUB.exec(asked.pathname);
      if (sub) {
        const own = radioStations(options.stations.store.list());
        const station = own.get(sub[1]!);
        const peer = station || !options.stations.peers ? undefined : radioPeers(own.keys(), await options.stations.peers.entries()).get(sub[1]!);
        if (!station && !peer) {
          json(response, 404, { error: "no station by that name", code: "not_found" });
          return;
        }
        if (sub[2] === "listen.m3u") {
          const name = (station ?? peer)!.name;
          const base = radioBaseFor(request, options.stations.publicBase?.() ?? "");
          response.writeHead(200, { ...CORS, "content-type": "audio/x-mpegurl; charset=utf-8", "content-disposition": `inline; filename="${sub[1]}.m3u"`, "cache-control": "no-store" });
          response.end(request.method === "HEAD" ? undefined : `#EXTM3U\n#EXTINF:-1,${name.replace(/[\r\n]/g, " ")}\n${base}/radio/${sub[1]}/stream\n`);
          return;
        }
        if (peer) {
          // A peer's station: asked of the peer with its token, passed on as it comes.
          if (request.method !== "GET" && request.method !== "HEAD") {
            json(response, 405, { error: "a station's link can listen, not drive", code: "method_not_allowed" });
            return;
          }
          const query = new URLSearchParams(asked.search);
          query.delete("t");
          query.set("t", peer.token);
          const stop = new AbortController();
          response.on("close", () => stop.abort());
          const answer = await fetch(`${peer.origin}/api/v1/stations/${peer.id}/${sub[2]}?${query.toString()}`, {
            method: request.method, signal: stop.signal,
            headers: { ...(request.headers.range ? { range: String(request.headers.range) } : {}), accept: String(request.headers.accept ?? "*/*") },
          }).catch(() => null);
          if (!answer) {
            json(response, 502, { error: "that station's server did not answer", code: "upstream" });
            return;
          }
          rewriteResponse(response, radioRewriter({ id: peer.id, listenToken: peer.token }, sub[1]!));
          const headers: Record<string, string> = { ...CORS };
          for (const name of ["content-type", "cache-control", "content-length", "content-range", "accept-ranges", "x-accel-buffering", "retry-after"]) {
            const value = answer.headers.get(name);
            if (value) headers[name] = value;
          }
          response.writeHead(answer.status, headers);
          if (!answer.body || request.method === "HEAD") {
            response.end();
            return;
          }
          try {
            for await (const chunk of answer.body as unknown as AsyncIterable<Uint8Array>) {
              if (response.destroyed) break;
              if (!response.write(chunk)) await new Promise((done) => { response.once("drain", done); response.once("close", done); });
            }
          } catch {
            // The listener left, or the peer did.
          }
          response.end();
          return;
        }
        const query = new URLSearchParams(asked.search);
        query.delete("t");
        query.set("t", station!.listenToken);
        request.url = `/api/v1/stations/${station!.id}/${sub[2]}?${query.toString()}`;
        const here = options.stations.publicBase?.() ?? "";
        rewriteResponse(response, radioRewriter(station!, sub[1]!, /^https:\/\//.test(here) ? `${storeSite}/buy?server=${serverId(here)}&` : ""));
        return handle(request, response);
      }
    }
    const url = new URL(request.url ?? "/", "http://localhost");
    const path = url.pathname;
    const webSite = options.webSites?.get((request.headers.host ?? "").toLowerCase());
    const web = webSite?.web ?? options.web;
    const eventSite = webSite?.site ?? options.invites?.site;
    const accountSite = eventSite ?? options.site ?? "https://nixamp.com";
    const key = options.key ?? null;
    const behindProxy = options.behindProxy ?? false;
    const listenKey = options.listenKey ?? null;
    const channelParts = path.startsWith("/api/channels/")
      ? path.slice("/api/channels/".length).split("/")
      : null;
    const channelId = channelParts ? cleanId(channelParts[0]) : "";
    const channelAction = channelParts?.[1];
    let channelEvent: LiveEvent | null | undefined;
    const eventForChannel = async (): Promise<LiveEvent | null> => {
      if (channelEvent === undefined) {
        channelEvent = channelId ? await options.events?.byRoom(channelId) ?? null : null;
      }
      return channelEvent;
    };
    // What the compression routes need from this handler: how to answer,
    // how to count a stream, and whether this caller holds the controls.
    const compressionCtx = (): RouteContext | null =>
      options.compression
        ? {
            service: options.compression,
            json,
            readBody: (incoming) => readBody(incoming),
            cors: CORS,
            watch: (incoming, outgoing, track) => watch(incoming, outgoing, "stream", track),
            controls: async () => {
              if (key === null) return true;
              if (scopeOf(keyFrom(request, url), key, null) === "control") return true;
              if (options.owner) return (await options.owner.check(false, tokenFrom(request.headers))).allowed;
              return false;
            },
          }
        : null;

    if (request.method === "OPTIONS") {
      response.writeHead(204, CORS);
      response.end();
      return;
    }

    // Metered before anything is done for the request, so a caller over its
    // allowance costs nothing but this check. The throttle decides; this only
    // carries its refusal back through Node's response.
    if (options.throttle) {
      const refused = await options.throttle.handle(requestFor(request));
      if (refused) {
        await answerWith(response, refused);
        return;
      }
    }

    // Opening a share link is what hands a browser its key. It comes back as a
    // cookie, so every later fetch, EventSource and <audio src> carries it
    // without the page knowing anything about keys. Either key works here, and
    // which one was used decides what the browser can then do.
    const inPath = key !== null ? keyInPath(path) : null;
    if (inPath !== null) {
      const offered = inPath.key;
      // The path has to be telling the truth about the key it carries. A `/v/`
      // link holding the control key would read as view-only to whoever you
      // sent it to and hand them the controls, which is the whole reason for
      // naming the two shapes in the first place.
      //
      // A mismatch answers exactly as a wrong key does, so nothing is learned
      // from the difference between the two.
      if (scopeOf(offered, key, listenKey) !== inPath.wants) {
        json(response, 404, { error: "not found" });
        return;
      }
      response.writeHead(302, { ...CORS, "set-cookie": keyCookie(offered, (request.socket as { encrypted?: boolean }).encrypted === true || (options.secureCookies ?? false)), location: "/" });
      response.end();
      return;
    }

    // nixamp as an authorization server, and the watch parties that pairing
    // exists for. Before the share-key check for the same reason sign-in is:
    // a client site holds an account's token, never a server's key.
    if (options.authServer && options.accounts && await handleOAuthApi(request, response, url, {
      server: options.authServer,
      accounts: options.accounts,
      ...(options.parties ? { parties: options.parties } : {}),
      ...(options.handles ? { handles: options.handles } : {}),
      secureCookies: options.secureCookies ?? false,
    })) return;

    // A site here that is a client of nixamp.com: connect, callback, streams.
    if (options.links && options.accounts && options.authServer && await handleNixampLinkApi(request, response, url, {
      links: options.links,
      accounts: options.accounts,
      issuer: options.site ?? DEFAULT_DIRECTORY,
      site: accountSite,
      redirectUris: options.authServer.client(BACKTOSCHOOL_CLIENT.id)?.redirectUris ?? [],
      home: "/#settings",
      secureCookies: options.secureCookies ?? false,
      fetcher: options.links.fetcher,
    })) return;

    if (options.events && await handleLiveApi(request, response, url, {
      events: options.events,
      ...(options.eventWriter ? {eventWriter: options.eventWriter} : {}),
      ...(options.accounts ? { accounts: options.accounts } : {}),
      ...(options.layouts ? { layouts: options.layouts } : {}),
      ...(options.rooms ? { rooms: options.rooms } : {}),
      ...(options.tickets ? { tickets: options.tickets } : {}),
      ...(eventSite ? { site: eventSite } : {}),
      ...(options.invites?.email ? { email: options.invites.email } : {}),
      ...(options.onEventUpdated ? { onEventUpdated: options.onEventUpdated } : {}),
      ...(options.accountProfiles
        ? { hostProfile: async (userId: string) => hostCard(await options.accountProfiles!.of(userId), userId, accountSite) }
        : {}),
    })) return;

    // The page explaining the reminder texts. Public for the same reason the
    // webhook is: the reader is a carrier reviewing the number, or somebody
    // who just got a message and wants it to stop. Neither has a share link.
    if (path === OPT_IN_PATH && options.partyLine) {
      const body = optInPage();
      response.writeHead(200, {
        ...CORS,
        "content-type": "text/html; charset=utf-8",
        "content-length": Buffer.byteLength(body),
      });
      response.end(request.method === "HEAD" ? undefined : body);
      return;
    }

    // --- following a broadcaster ------------------------------------------
    //
    // Behind the sign-in rather than the share key: a follow belongs to an
    // account, and an account is the only thing that makes "notify me on my
    // other device" mean anything.
    // --- names and certificates for an account's servers --------------------
    //
    // A server that is signed in becomes `<label>.<handle>.<zone>`, with A and
    // AAAA records nixamp.com writes with keys only nixamp.com holds, and it
    // serves https with the one wildcard certificate its handle has. Nothing
    // about DNS or ACME ever reaches the box; it asks, and is answered.
    if ((path === "/api/v1/dns" || path.startsWith("/api/v1/dns/")) && options.names && options.accounts && options.handles) {
      const me = await options.accounts.whoIs(tokenFrom(request.headers));
      if (me === null) {
        json(response, 401, { error: "sign in to name a server" });
        return;
      }
      const handle = await options.handles.of(me.id);
      if (!handle) {
        json(response, 422, { error: "this account has no handle yet" });
        return;
      }
      const names = options.names;
      const zone = `${handle}.${options.dnsZone ?? ""}`.replace(/\.$/, "");

      if (path === "/api/v1/dns" && request.method === "GET") {
        json(response, 200, { zone, names: await names.list(me.id, handle) });
        return;
      }
      const label = decodeURIComponent(path.slice("/api/v1/dns/".length));
      if (!label) {
        json(response, 404, { error: "no such endpoint" });
        return;
      }
      if (request.method === "PUT" || request.method === "POST") {
        let body: { a?: unknown; aaaa?: unknown; ttl?: unknown } = {};
        try {
          body = JSON.parse((await readBody(request)) || "{}") as typeof body;
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        // "auto" is the address this request came from, for whichever family
        // it came in on: a server names itself without knowing its address.
        const caller = callerOf(request.headers, request.socket.remoteAddress, options.behindProxy ?? false);
        const family = (value: unknown, is: (ip: unknown) => boolean): string | null | undefined => {
          if (value === null) return null;
          if (value === undefined) return undefined;
          if (value === "auto") return is(caller) ? caller : undefined;
          return String(value);
        };
        try {
          const name = await names.set(me.id, handle, label, {
            a: family(body.a, isIPv4),
            aaaa: family(body.aaaa, isIPv6),
            ...(typeof body.ttl === "number" ? { ttl: body.ttl } : {}),
          });
          json(response, 200, { name });
        } catch (error) {
          const status = error instanceof NameError ? error.status : 500;
          json(response, status, { error: (error as Error).message });
        }
        return;
      }
      if (request.method === "DELETE") {
        const gone = await names.remove(me.id, handle, label);
        json(response, gone ? 200 : 404, gone ? { ok: true } : { error: "no such name of yours" });
        return;
      }
      json(response, 405, { error: "GET, PUT or DELETE" });
      return;
    }

    if (path === "/api/v1/certs" && options.certs && options.accounts && options.handles) {
      const me = await options.accounts.whoIs(tokenFrom(request.headers));
      if (me === null) {
        json(response, 401, { error: "sign in to get a certificate" });
        return;
      }
      const handle = await options.handles.of(me.id);
      if (!handle) {
        json(response, 422, { error: "this account has no handle yet" });
        return;
      }
      const state = await options.certs.forHandle(handle);
      const host = `*.${handle}.${options.dnsZone ?? ""}`.replace(/\.$/, "");
      if (state.status === "ready") {
        json(response, 200, { status: "ready", cert: state.cert, key: state.key, expiresAt: state.expiresAt, host, renewing: state.renewing });
        return;
      }
      if (state.status === "failed") {
        json(response, 503, { status: "failed", error: state.error, host });
        return;
      }
      json(response, 202, { status: "issuing", host });
      return;
    }

    // --- an invite: "so-and-so is streaming", by text or by email ------------
    //
    // The page had a Send button and nothing answered it: the message was
    // written (invite.ts) and never given a route. Sending needs an account,
    // because a text costs money and lands on somebody's phone, and the link
    // sent is always the listen link -- an admin link handed out by mistake
    // would hand out the server.
    if (path === "/api/v1/invite" && options.invites && options.accounts) {
      if (request.method !== "POST") {
        json(response, 405, { error: "POST only" });
        return;
      }
      const me = await options.accounts.whoIs(tokenFrom(request.headers));
      if (me === null) {
        json(response, 401, { error: "sign in to send an invite" });
        return;
      }
      let body: { to?: unknown; stream?: unknown } = {};
      try {
        body = JSON.parse(await readBody(request)) as typeof body;
      } catch {
        json(response, 400, { error: "bad JSON" });
        return;
      }
      const to = String(body.to ?? "").trim();
      const stream = String(body.stream ?? "").trim();
      const byEmail = isEmail(to);
      const bySms = !byEmail && isPhone(to);
      if (!byEmail && !bySms) {
        json(response, 400, { error: "give a phone number or an email address" });
        return;
      }
      let origin = "";
      try {
        origin = new URL(stream).origin;
      } catch {
        json(response, 400, { error: "that is not a stream link" });
        return;
      }
      // What the directory knows about this server names it and gives the
      // listen link and the phone code. An unlisted server is still sendable,
      // as long as the link given is not the one that drives it.
      const listed = options.directory?.list().find((one) => {
        try {
          return new URL(one.url).origin === origin;
        } catch {
          return false;
        }
      });
      const link = listed?.url ?? stream;
      if (/\/admin\//.test(link) || /[?&]k=/.test(link) && !listed) {
        json(response, 400, { error: "send the view link, not the admin link" });
        return;
      }
      const invite = {
        name: listed?.name ?? new URL(link).hostname,
        link: watchLink(link, options.invites.site),
        phone: listed ? CALL_IN_NUMBER : "",
        code: listed?.code ?? "",
      };
      const sender = byEmail ? options.invites.email : options.invites.sms;
      if (!sender) {
        json(response, 503, { error: byEmail ? "this site cannot send email yet" : "this site cannot send texts yet" });
        return;
      }
      const ok = byEmail
        ? await options.invites.email!(to, { title: inviteSubject(invite), body: inviteText(invite), url: invite.link })
        : await options.invites.sms!.send(to, inviteText(invite));
      json(response, ok ? 200 : 502, ok ? { sent: to } : { error: "the message did not go" });
      return;
    }

    // Favourites: the servers you hearted, kept against your account. Reading
    // the directory and listening need no account; remembering where you
    // listened does, because there has to be somebody to remember it for.
    if (path === "/api/v1/favorites" && options.favorites && options.accounts) {
      const me = await options.accounts.whoIs(tokenFrom(request.headers));
      if (me === null) {
        json(response, 401, { error: "sign in to keep favourites" });
        return;
      }
      const favorites = options.favorites;

      if (request.method === "GET") {
        const list = await favorites.list(me.id);
        // Whether each is on right now, from the directory: a favourite that
        // is live is the one to click first.
        const live = new Map((options.directory?.list() ?? []).map((one) => [one.url, one]));
        json(response, 200, {
          favorites: list.map((one) => {
            const on = live.get(one.url);
            return {
              ...one,
              live: on !== undefined,
              nowPlaying: on?.playing ? on.nowPlaying : "",
              channels: on?.channels ?? [],
            };
          }),
        });
        return;
      }

      if (request.method === "PUT" || request.method === "POST") {
        let body: { url?: unknown; name?: unknown } = {};
        try {
          body = JSON.parse(await readBody(request)) as typeof body;
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        const address = favoriteUrl(body.url);
        if (!address) {
          json(response, 400, { error: "give the server's address" });
          return;
        }
        await favorites.add(me.id, address, typeof body.name === "string" ? body.name : "");
        json(response, 200, { favorite: true });
        return;
      }

      if (request.method === "DELETE") {
        const address = favoriteUrl(url.searchParams.get("url"));
        if (!address) {
          json(response, 400, { error: "give the server's address" });
          return;
        }
        await favorites.remove(me.id, address);
        json(response, 200, { favorite: false });
        return;
      }
      json(response, 405, { error: "GET, PUT or DELETE" });
      return;
    }

    // --- settings sync: your settings on every machine, against the account ----
    //
    // The compression policy and the remembered channels, as one snapshot
    // under a revision (@profullstack/synconfig). The directory stores what
    // the client sent and hands it back; it never reads the files inside.
    if ((path === "/api/v1/settings" || path === "/api/v1/settings/revisions") && options.settingsSync && options.accounts) {
      const me = await options.accounts.whoIs(tokenFrom(request.headers));
      if (me === null) {
        json(response, 401, { error: "sign in to sync settings" });
        return;
      }
      let body: unknown;
      if (request.method === "PUT") {
        try {
          body = JSON.parse(await readBody(request, SETTINGS_BODY_LIMIT)) as unknown;
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
      }
      const reply = await options.settingsSync.handle(request.method ?? "GET", path, me.id, body);
      json(response, reply.status, reply.body);
      return;
    }

    // Following an artist from a /radio page, where the nixamp.com session is first-party (follow-page.ts).
    if (path === "/follow" && options.follows && options.accounts) {
      const accounts = options.accounts;
      if (await handleFollowPage(request, response, path, url, {
        account: () => accounts.whoIs(tokenFrom(request.headers)),
        follows: options.follows,
        readBody: (body) => readBody(body, 8 * 1024),
      })) return;
    }

    if (path.startsWith("/api/v1/follows") && options.follows && options.accounts) {
      const me = await options.accounts.whoIs(tokenFrom(request.headers));
      if (me === null) {
        json(response, 401, { error: "sign in to follow" });
        return;
      }
      const follows = options.follows;

      if (path === "/api/v1/follows" && request.method === "GET") {
        const ids = await follows.following(me.id);
        // An id is not a name. The directory is the only thing that knows what
        // an account calls itself, from the last stream it announced -- which
        // is empty for somebody who has never streamed, and the caller decides
        // what to show for that rather than being handed a blank.
        json(response, 200, {
          following: ids.map((id) => ({
            id,
            name: options.directory?.nameOf(id) ?? "",
            live: options.directory?.isLive(id) ?? false,
          })),
        });
        return;
      }

      const streamer = path.slice("/api/v1/follows/".length);
      if (!path.startsWith("/api/v1/follows/") || !streamer) {
        json(response, 404, { error: "no such endpoint" });
        return;
      }

      if (request.method === "PUT" || request.method === "POST") {
        const added = await follows.follow(me.id, decodeURIComponent(streamer));
        // Following yourself is refused rather than silently stored: you do
        // not need telling that you went live.
        json(response, added ? 200 : 422, added
          ? { following: true, followers: await follows.followerCount(decodeURIComponent(streamer)) }
          : { error: "you cannot follow yourself" });
        return;
      }
      if (request.method === "DELETE") {
        await follows.unfollow(me.id, decodeURIComponent(streamer));
        json(response, 200, { following: false });
        return;
      }
      if (request.method === "GET") {
        json(response, 200, { following: await follows.isFollowing(me.id, decodeURIComponent(streamer)) });
        return;
      }
      json(response, 405, { error: "PUT, DELETE or GET" });
      return;
    }

    // --- where to reach a follower ----------------------------------------
    if (path.startsWith("/api/v1/notify") && options.follows && options.accounts) {
      // The key is public by design: it is what a browser needs before it can
      // ask permission, and it is useless without the private half. Served at
      // run time, never built into the page: a key baked in at build time is
      // how push quietly becomes "not supported" in production. 503 when this
      // instance has no keys, so the browser can say so.
      if (path === "/api/v1/notify/key" && request.method === "GET") {
        await answerWith(response, vapidPublicKeyResponse(options.vapidKeys));
        return;
      }

      const me = await options.accounts.whoIs(tokenFrom(request.headers));
      if (me === null) {
        json(response, 401, { error: "sign in first" });
        return;
      }
      const follows = options.follows;

      if (path === "/api/v1/notify/prefs") {
        if (request.method === "GET") {
          json(response, 200, await follows.prefs(me.id));
          return;
        }
        if (request.method !== "PUT" && request.method !== "POST") {
          json(response, 405, { error: "GET or PUT" });
          return;
        }
        let body: Record<string, unknown>;
        try {
          body = JSON.parse(await readBody(request)) as Record<string, unknown>;
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        // A number we cannot dial is worse than no number: it is a text that
        // silently goes nowhere for as long as nobody checks.
        if (body["phone"] !== undefined && body["phone"] !== "" && !phoneFrom(body["phone"])) {
          json(response, 422, { error: "that does not look like a phone number" });
          return;
        }
        await follows.setPrefs(me.id, {
          ...(body["phone"] === undefined ? {} : { phone: String(body["phone"]) }),
          ...(typeof body["wantsEmail"] === "boolean" ? { wantsEmail: body["wantsEmail"] } : {}),
          ...(typeof body["wantsSms"] === "boolean" ? { wantsSms: body["wantsSms"] } : {}),
          ...(typeof body["wantsWeb"] === "boolean" ? { wantsWeb: body["wantsWeb"] } : {}),
        });
        json(response, 200, await follows.prefs(me.id));
        return;
      }

      if (path === "/api/v1/notify/subscribe") {
        if (request.method === "DELETE") {
          // The endpoint arrives in the query (older pages) or as the JSON
          // body @profullstack/notifications sends: { endpoint }.
          let endpoint = url.searchParams.get("endpoint") ?? "";
          if (!endpoint) {
            try {
              const body = JSON.parse(await readBody(request)) as { endpoint?: unknown };
              if (typeof body.endpoint === "string") endpoint = body.endpoint;
            } catch {
              // No body: nothing to forget.
            }
          }
          await follows.removePush(endpoint);
          json(response, 200, { ok: true });
          return;
        }
        if (request.method !== "POST" && request.method !== "PUT") {
          json(response, 405, { error: "POST or DELETE" });
          return;
        }
        let body: { endpoint?: unknown; keys?: { p256dh?: unknown; auth?: unknown } };
        try {
          body = JSON.parse(await readBody(request)) as typeof body;
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        const endpoint = typeof body.endpoint === "string" ? body.endpoint : "";
        const p256dh = typeof body.keys?.p256dh === "string" ? body.keys.p256dh : "";
        const auth = typeof body.keys?.auth === "string" ? body.keys.auth : "";
        if (!endpoint || !p256dh || !auth) {
          json(response, 422, { error: "a subscription needs an endpoint and both keys" });
          return;
        }
        await follows.addPush(me.id, { endpoint, p256dh, auth });
        json(response, 200, { ok: true });
        return;
      }

      json(response, 404, { error: "no such endpoint" });
      return;
    }

    // --- CoinPay ----------------------------------------------------------
    //
    // Ahead of the share-key check because the caller is CoinPay, which signs
    // the body instead. The body is only a nudge: settle() asks CoinPay for the
    // payment itself before anything is credited.
    if (path === "/api/v1/webhooks/coinpay") {
      if (request.method !== "POST") { json(response, 405, { error: "POST only" }); return; }
      const secret = options.coinpayWebhookSecret ?? "";
      if (!secret) { json(response, 503, { error: "CoinPay webhooks are not configured here." }); return; }
      let raw: string;
      try { raw = await readBody(request); } catch { json(response, 413, { error: "body too large" }); return; }
      const signature = request.headers["x-coinpay-signature"];
      if (!verifyCoinpaySignature(raw, Array.isArray(signature) ? signature[0] : signature, secret)) {
        json(response, 401, { error: "invalid signature" }); return;
      }
      let event: ReturnType<typeof readCoinpayWebhook>;
      try { event = readCoinpayWebhook(JSON.parse(raw)); } catch { json(response, 400, { error: "invalid JSON" }); return; }
      if (options.musicStore) {
        try {
          if (await options.musicStore.owns(event.paymentId)) { json(response, 200, await options.musicStore.settle(event.paymentId)); return; }
          if (options.musicLicenses && await options.musicLicenses.owns(event.paymentId)) { json(response, 200, await options.musicLicenses.settle(event.paymentId)); return; }
        } catch (error) {
          console.error(`nixamp: CoinPay webhook for ${event.paymentId} (music) could not settle: ${error instanceof Error ? error.message : String(error)}`);
          json(response, 500, { error: "could not settle; retry" });
          return;
        }
      }
      if (!options.translationPasses) { json(response, 200, { settled: false, reason: "billing is off here" }); return; }
      try {
        json(response, 200, await options.translationPasses.settle(event.paymentId));
      } catch (error) {
        // A 5xx makes CoinPay deliver it again, which is what an unreachable
        // CoinPay or database needs.
        console.error(`nixamp: CoinPay webhook for ${event.paymentId} could not settle: ${error instanceof Error ? error.message : String(error)}`);
        json(response, 500, { error: "could not settle; retry" });
      }
      return;
    }

    // --- the party line ---------------------------------------------------
    //
    // Ahead of the share-key check because the caller is a telephone. Telnyx
    // has no cookie and no link; what it has is an ed25519 signature over the
    // body, which is a stronger claim than a key in a URL anyway.
    if (path.startsWith("/api/v1/partyline/") && options.partyLine) {
      const partyLine = options.partyLine;

      if (path === "/api/v1/partyline/rooms") {
        json(response, 200, { rooms: partyLine.list() });
        return;
      }

      if (path !== "/api/v1/partyline/webhook") {
        json(response, 404, { error: "no such endpoint" });
        return;
      }
      if (request.method !== "POST") {
        json(response, 405, { error: "POST only" });
        return;
      }

      // The bytes as they arrived. Parsing first and reserialising would
      // change the whitespace the signature was computed over.
      let raw: string;
      try {
        raw = await readBody(request);
      } catch {
        json(response, 413, { error: "body too large" });
        return;
      }

      const signature = request.headers["telnyx-signature-ed25519"];
      const timestamp = request.headers["telnyx-timestamp"];
      const ok = partyLine.verify(
        raw,
        typeof signature === "string" ? signature : undefined,
        typeof timestamp === "string" ? timestamp : undefined,
      );
      if (!ok) {
        json(response, 401, { error: "bad signature" });
        return;
      }

      let event: { data?: { event_type?: string; payload?: Record<string, unknown> } };
      try {
        event = JSON.parse(raw) as typeof event;
      } catch {
        json(response, 400, { error: "bad JSON" });
        return;
      }

      // Answer first, act second. Telnyx retries anything it does not hear
      // back about quickly, and a retried call.answered would ask the caller
      // which room they wanted twice.
      json(response, 200, { ok: true });
      void partyLine.handle(event.data ?? {}).catch(() => {});
      return;
    }

    // An advert for a break.
    //
    // Answered here, before the key check, because the listeners who get
    // adverts are precisely the ones with no key and no session. The player
    // asks for this when a break is due and treats anything but a url as "no
    // advert", so every failure below is a silent null: a break nobody can
    // fill simply does not happen and the listener keeps their music.
    //
    // Nothing about which advert to play is decided here. That is crawlproof's
    // auction, and it meters the impression — so this must not cache, retry, or
    // ask twice for one break.
    if (path === "/api/ads/next" && request.method === "GET") {
      json(response, 200, await nextAdvert(url.searchParams.get("kind")));
      return;
    }

    // What this listener has paid for.
    //
    // Nothing yet, and saying so is the point. The player asks this before
    // every break to decide whether to run one; with no route it asked, got a
    // 404 on every page load, and read the miss as "holds nothing" — the right
    // answer by the wrong road, and a console error that looked like a fault
    // to anybody who opened the devtools.
    //
    // An empty list is the honest reply while nixamp has no OpenAccess client:
    // there is no pass to hold, so nobody holds one. The shape is the one the
    // player already reads, so wiring the hub later is a change here and
    // nowhere else.
    if (path === "/api/entitlements" && request.method === "GET") {
      json(response, 200, { entitlements: [] });
      return;
    }

    // /api/health answers unauthenticated on purpose: it is how you check the
    // port is open from another device before wondering whether the link is
    // wrong, and it says nothing about the library.
    const eventChannelRequest = Boolean(channelId && await eventForChannel());
    // The public station guide: at /guide, and at the front of a host named
    // for it (a Moshpit name such as live.streaming, NIXAMP_GUIDE_HOSTS).
    const requestHost = String(request.headers["x-forwarded-host"] ?? request.headers.host ?? "").split(",")[0]!.trim().replace(/:\d+$/, "").toLowerCase();
    const isGuidePage = Boolean(options.stations) && (request.method === "GET" || request.method === "HEAD") &&
      (path === "/guide" || (path === "/" && guideHosts().has(requestHost)));
    // nixamp.com's own /radio is the radio app (web/radio.html), not live.streaming's
    // station list; its other /radio pages name nixamp.com, not the box behind it.
    const onDirectory = (() => { try { return new URL(storeSite).host === requestHost; } catch { return false; } })();
    const radioPath = isRadioPath(path) && !(onDirectory && (path === "/radio" || path === "/radio/"));
    // The built web bundle is the same public code nixamp.com hands anyone;
    // a station's page loads it, and a station link carries no server key.
    const isPublicAsset = (request.method === "GET" || request.method === "HEAD") &&
      (/^\/assets\/[A-Za-z0-9._-]+$/.test(path) || path === "/favicon.png");
    // Whether this request holds only the listen key. A listener may browse
    // and hear the library, but where it lives on this machine is the
    // owner's business: the absolute path names the user account and the disk.
    let listening = false;
    if (
      key !== null &&
      path !== "/api/health" &&
      path !== "/api/directory" &&
      !isSignInPath(path) &&
      // A station's link is its own key, checked by the station.
      !isStationListenPath(path) &&
      // Sharing a track and reporting one are for anybody the guide reaches.
      !isPublicSharePath(path) &&
      // The AI label's catalogue, its songs and their words.
      !(isPublicLabelPath(path) && Boolean(options.stations?.label)) &&
      // The music store: its pages, checkout, receipts and grants (nixamp.com).
      !(isStorePath(path) && Boolean(options.musicStore)) &&
      !(isLicensePath(path) && Boolean(options.musicLicenses)) &&
      // nixamp.com/music: the app and its catalogue, for anybody.
      !(isMusicPath(path) && Boolean(options.music)) &&
      !(/^\/radio\/(artist|album)\//.test(path) && Boolean(options.music)) &&
      // What is on, as MCP, for anybody.
      path !== "/mcp/public" &&
      // Supporting a copyright holder is for anybody listening.
      !/^\/support\/[a-z0-9-]{1,40}$/.test(path) && path !== "/support/cancel" && !/^\/api\/v1\/support\/[a-z0-9-]{1,40}$/.test(path) &&
      // A station's short link, which sends the browser to wherever it plays.
      !/^\/watch\/[a-z0-9]{10}$/.test(path) &&
      // A live room's page: the room's own key travels in ?play=, checked by its server.
      !(path === "/watch" && Boolean(options.stations?.rooms)) &&
      // A live room under its name here, asked of its server with the key held here (guide-rooms.ts).
      !(isRoomPath(path) && Boolean(options.stations?.rooms)) &&
      // Live rooms on the guide: listed for anybody, published by a member (guide-rooms.ts).
      !((path === "/api/v1/guide/rooms" || path.startsWith("/api/v1/guide/rooms/")) && Boolean(options.stations?.rooms)) &&
      // On demand: a file's page and the guide's list of them (guide-vods.ts); the file's own key travels in its play address.
      !((path === "/vod" || /^\/vod\/[A-Za-z0-9_-]{1,32}$/.test(path) || path === "/api/v1/guide/vods" || path.startsWith("/api/v1/guide/vods/")) && Boolean(options.stations?.vods)) &&
      !STATION_PAGE.test(path) &&
      // Public, listed stations under readable names, and their artists and albums (radio-pages.ts).
      !(radioPath && Boolean(options.stations)) &&
      // Following an artist from those pages, signed in at nixamp.com.
      !(path === "/follow" && Boolean(options.follows && options.accounts)) &&
      !isPublicAsset &&
      !isGuidePage &&
      !eventChannelRequest
    ) {
      let scope = scopeOf(keyFrom(request, url), key, listenKey);
      // Kept for the routes below that answer a listener with less.
      const settle = (): void => { listening = scope === "listen"; };
      // A key is how somebody who was invited proves it. It is not the only way
      // to be allowed in: the person who owns this server is allowed in whether
      // or not they still have the link, and their nixamp.com session says who
      // they are. Without this, signing in as yourself and opening your own
      // server was refused, and the address of a machine you administer was
      // useless without a link you had to go and find.
      if (scope === null && options.owner) {
        // In the query too, as controlOf takes it: an <audio> on another
        // origin (nixamp.com/radio) cannot send a header.
        const check = await options.owner.check(false, tokenFrom(request.headers) || url.searchParams.get("session") || "");
        if (check.allowed) scope = "control";
      }
      if (scope === null) {
        // Counted, not because a 128-bit key falls to guessing, but because
        // somebody hammering one should stop costing this server anything.
        const who = callerOf(request.headers, request.socket.remoteAddress, behindProxy);
        const verdict = guard.check(`key:${who}`, BAD_KEY_LIMIT);
        if (!verdict.ok) {
          response.writeHead(429, { ...CORS, "content-type": "application/json; charset=utf-8", "retry-after": String(verdict.retryAfter) });
          response.end(JSON.stringify({ error: "too many attempts; wait a moment" }));
          return;
        }
        json(response, 401, {
          error: "this nixamp needs the key from its share link, or sign in as its owner",
        });
        return;
      }
      settle();
      if (scope === "listen" && !allowedForListening(path)) {
        json(response, 403, { error: "this link can listen, not drive" });
        return;
      }
    }

    // Whether this request may administer: the control key, an open server,
    // or the owner's nixamp.com session. Asked by the routes that do not go
    // through the admin gate further down.
    const controlOf = async (): Promise<{ allowed: boolean; account: string }> => {
      if (key === null) return { allowed: true, account: "" };
      if (scopeOf(keyFrom(request, url), key, listenKey) === "control") return { allowed: true, account: "" };
      if (options.owner) {
        const session = tokenFrom(request.headers) || url.searchParams.get("session") || "";
        if (session !== "" && (await options.owner.check(false, session)).allowed) {
          return { allowed: true, account: (await options.owner.accountFor(session)) || "" };
        }
      }
      return { allowed: false, account: "" };
    };

    /*
     * /watch/<station id>: a short link that works on any server with a
     * guide -- nixamp.com, or a guide host like live.streaming -- for a
     * public, listed station here or on a peer (on nixamp.com, every server
     * in the directory). It sends the browser to the station's own page, on
     * the server that plays it. Nothing private or unlisted is ever found.
     */
    /* A monthly support's cancel link, from any of its emails. */
    if (path === "/support/cancel" && options.supportSubscriptions && (request.method === "GET" || request.method === "HEAD")) {
      let words: string;
      try {
        const done = await options.supportSubscriptions.cancel(url.searchParams.get("s") ?? "", url.searchParams.get("t") ?? "");
        const name = options.support?.holder(done.holder)?.name ?? done.holder;
        words = `Your monthly support for ${name} is cancelled. No more payment links will be sent.`;
      } catch (error) {
        words = error instanceof SupportError ? error.message : "That did not work; try the link again.";
      }
      const safe = words.replace(/[&<>"']/g, (one) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[one]!);
      response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "content-security-policy": "default-src 'none'; style-src 'unsafe-inline'", "referrer-policy": "no-referrer" });
      response.end(request.method === "HEAD" ? undefined : `<!doctype html><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Support · nixamp</title><body style="margin:0;background:#080c09;color:#e8efe9;font:16px/1.5 system-ui,sans-serif"><main style="max-width:520px;margin:0 auto;padding:48px 16px"><h1 style="font-size:1.4rem">Monthly support</h1><p>${safe}</p><p><a style="color:#3ddc84" href="https://nixamp.com/guide">Back to the stations</a></p></main></body>`);
      return;
    }

    /* Support a copyright holder: the page, the coins, and a CoinPay checkout. */
    const supportPage = /^\/support\/([a-z0-9-]{1,40})$/.exec(path);
    if (supportPage && options.support && (request.method === "GET" || request.method === "HEAD")) {
      if (!options.support.holder(supportPage[1]!)) {
        json(response, 404, { error: "nobody by that name is supported here", code: "not_found" });
        return;
      }
      response.writeHead(200, { "content-type": "text/html; charset=utf-8", "cache-control": "no-cache", "content-security-policy": SUPPORT_CSP, "referrer-policy": "no-referrer", "x-content-type-options": "nosniff" });
      response.end(request.method === "HEAD" ? undefined : SUPPORT_PAGE);
      return;
    }
    const supportApi = /^\/api\/v1\/support\/([a-z0-9-]{1,40})$/.exec(path);
    if (supportApi && options.support) {
      const support = options.support;
      const holder = support.holder(supportApi[1]!);
      if (!holder) {
        json(response, 404, { error: "nobody by that name is supported here", code: "not_found" });
        return;
      }
      try {
        if (request.method === "GET") {
          const { coins } = support.enabled ? await support.coins() : { coins: [] as string[] };
          json(response, 200, { id: holder.id, name: holder.name, coins, ...(support.enabled ? {} : { error: "Support payments are not set up here." }) });
          return;
        }
        if (request.method === "POST") {
          const verdict = guard.check(`support:${callerOf(request.headers, request.socket.remoteAddress, behindProxy)}`, { allowed: 10, windowMs: 60_000 });
          if (!verdict.ok) {
            json(response, 429, { error: "too many at once; wait a minute", code: "rate_limited", retryAfter: verdict.retryAfter });
            return;
          }
          let body: Record<string, unknown> = {};
          try { body = JSON.parse(await readBody(request)) as Record<string, unknown>; } catch { body = {}; }
          if (body["monthly"] === true) {
            if (!options.supportSubscriptions) throw new SupportError("Monthly support is not set up here.", 503, "unavailable");
            json(response, 200, await options.supportSubscriptions.start({ ...body, holder: holder.id }));
          } else {
            json(response, 200, await support.checkout({ ...body, holder: holder.id }));
          }
          return;
        }
        json(response, 405, { error: "GET or POST", code: "method_not_allowed" });
      } catch (error) {
        if (error instanceof SupportError) json(response, error.status, { error: error.message, code: error.code });
        else json(response, 502, { error: "the payment service did not answer; nothing was charged", code: "provider" });
      }
      return;
    }

    const stationShortLink = /^\/watch\/([a-z0-9]{10})$/.exec(path);
    if (stationShortLink && options.stations && (request.method === "GET" || request.method === "HEAD")) {
      const encrypted = (request.socket as { encrypted?: boolean }).encrypted === true;
      const own = guideEntries({
        store: options.stations.store, runner: options.stations.runner, channels: options.channels!, canControl: false, account: "",
        select: () => null, trackFor: (file) => trackFor(engine, file),
        base: `${encrypted ? "https" : "http"}://${request.headers.host ?? "localhost"}`,
        publicBase: options.stations.publicBase?.() ?? "", json, readBody, headers: CORS,
      });
      const found = own.find((one) => one.id === stationShortLink[1])
        ?? (options.stations.peers ? (await options.stations.peers.entries()).find((one) => one.id === stationShortLink[1]) : undefined);
      if (!found || !/^https?:\/\//.test(found.page)) {
        json(response, 404, { error: "no public station with that id", code: "not_found" });
        return;
      }
      // Its /radio name, ours or a peer's (radio-pages.ts): no server address or token in the link it lands on.
      let location = "radio" in found && typeof found.radio === "string" && /^https?:\/\//.test(found.radio) ? found.radio : "";
      if (!location && options.stations.peers) {
        const named = radioPeers(radioStations(options.stations.store.list()).keys(), await options.stations.peers.entries());
        const slug = [...named].find(([, one]) => one.id === stationShortLink[1])?.[0];
        if (slug) location = `${watchBase(options.stations.publicBase?.() || `https://${request.headers.host ?? "localhost"}`)}/radio/${slug}`;
      }
      response.writeHead(302, { location: location || found.page, "cache-control": "no-store", "referrer-policy": "no-referrer" });
      response.end();
      return;
    }

    if (isGuidePage) {
      const encrypted = (request.socket as { encrypted?: boolean }).encrypted === true;
      const entries = guideEntries({
        store: options.stations!.store,
        runner: options.stations!.runner,
        channels: options.channels!,
        canControl: false,
        account: "",
        select: () => null,
        trackFor: (file) => trackFor(engine, file),
        base: `${encrypted ? "https" : "http"}://${request.headers.host ?? "localhost"}`,
        publicBase: options.stations!.publicBase?.() ?? "",
        json,
        readBody,
        headers: CORS,
      });
      const own = new Set(entries.map((one) => one.page));
      const others = options.stations!.peers ? (await options.stations!.peers.entries()).filter((one) => !own.has(one.page)) : [];
      const rooms = options.stations!.rooms
        ? await roomEntries(options.stations!.rooms.store, options.stations!.rooms.probe, watchBase(options.stations!.publicBase?.() || `${encrypted ? "https" : "http"}://${request.headers.host ?? "localhost"}`))
        : [];
      const ads = guideAds();
      const label = options.stations!.label;
      // A peer's station by its /radio name here too, asked of the peer by this server (radio-pages.ts).
      const radioBase = watchBase(options.stations!.publicBase?.() || `https://${request.headers.host ?? "localhost"}`);
      const peerNames = radioPeers(radioStations(options.stations!.store.list()).keys(), others);
      const peerRadio = new Map([...peerNames].map(([slug, one]) => [`${one.origin}/station/${one.id}`, `${radioBase}/radio/${slug}`]));
      const named = others.map((one) => {
        const at = (() => { try { const u = new URL(one.page); return `${u.origin}${u.pathname.replace(/\/+$/, "")}`; } catch { return ""; } })();
        const radio = peerRadio.get(at);
        return radio ? { ...one, radio } : one;
      });
      // Behind a guide name (live.streaming) the page asks its own origin, so no server address shows in it.
      const apiBase = guideHosts().has(requestHost) ? "" : options.stations!.publicBase?.() ?? "";
      const page = renderGuide([...rooms, ...entries, ...named] as GuideEntry[], apiBase, ads, label && label.publicView().genres.length > 0 ? "/label" : "", path === "/");
      response.writeHead(200, {
        "content-type": "text/html; charset=utf-8",
        "cache-control": "no-cache",
        "content-security-policy": guideCsp(ads),
        "referrer-policy": "no-referrer",
        "x-content-type-options": "nosniff",
      });
      response.end(request.method === "HEAD" ? undefined : page);
      return;
    }

    // The page a station's link opens. The same bytes for every station: it
    // reads its id and token from its own address.
    // A live room's page: /watch?play=<a nixamp server's channel or live
    // stream address>. The built page plays it in @profullstack/player, as a
    // film or as radio; nothing about the room passes through here.
    if (options.stations?.rooms && isRoomPath(path)) {
      const rooms = options.stations.rooms;
      if (await handleRoomProxy(request, response, path, { store: rooms.store, probe: rooms.probe, refuse: (link) => refusedDestination(link), json })) return;
      const asked = ROOM_PAGE.exec(path)![1]!;
      const slugs = roomSlugs(rooms.store.list());
      const room = slugs.get(asked);
      if (!room) {
        // A room's id finds its name: /room/<id> goes to /room/<name>.
        const named = roomSlugOf(slugs, asked);
        if (named) {
          response.writeHead(301, { location: `/room/${named}`, "cache-control": "public, max-age=300" });
          response.end();
          return;
        }
      }
      if (request.method !== "GET" && request.method !== "HEAD") {
        json(response, 405, { error: "method not allowed", code: "method_not_allowed" });
        return;
      }
      const built = web !== null && isFile(join(web, "watch.html")) ? readFileSync(join(web, "watch.html"), "utf8") : null;
      if (!built) {
        json(response, 404, { error: "this server was started without its web pages" });
        return;
      }
      const base = radioBaseFor(request, options.stations.publicBase?.() ?? "");
      const nav = radioNav(guideHosts().has(requestHost) ? "/" : "/guide", "live");
      const face = room ? { name: room.name, description: room.note || `${room.name}, live now on live.streaming.`, video: room.kind === "video" } : { name: "No such room" };
      response.writeHead(room ? 200 : 404, {
        "content-type": "text/html; charset=utf-8",
        "cache-control": "no-cache",
        "content-security-policy": adCsp(WATCH_APP_CSP, guideAds()),
        "referrer-policy": "strict-origin-when-cross-origin",
        "x-content-type-options": "nosniff",
      });
      response.end(request.method === "HEAD" ? undefined : withPageAd(asRadioPage(built, face, asked, base, nav, `/room/${asked}`), guideAds()));
      return;
    }

    if (options.stations?.rooms && path === "/watch" && (request.method === "GET" || request.method === "HEAD")) {
      // A room published here plays under its name: the old link keeps working and lands there.
      const play = roomAddress(url.searchParams.get("play") ?? "");
      if (play) {
        const slugs = roomSlugs(options.stations.rooms.store.list());
        const published = [...slugs].find(([, one]) => one.play === play.play)?.[0];
        if (published) {
          response.writeHead(302, { location: `/room/${published}`, "cache-control": "no-store", "referrer-policy": "no-referrer" });
          response.end();
          return;
        }
      }
      const built = web !== null && isFile(join(web, "watch.html")) ? withPageAd(readFileSync(join(web, "watch.html"), "utf8"), guideAds()) : null;
      if (!built) {
        json(response, 404, { error: "this server was started without its web pages" });
        return;
      }
      response.writeHead(200, {
        "content-type": "text/html; charset=utf-8",
        "cache-control": "no-cache",
        "content-security-policy": adCsp(WATCH_APP_CSP, guideAds()),
        "referrer-policy": "no-referrer",
        "x-content-type-options": "nosniff",
      });
      response.end(request.method === "HEAD" ? undefined : built);
      return;
    }

    // A file on demand: /vod (all of them) and /vod/<id> (one), the page a VOD link opens.
    const vodPath = /^\/vod(?:\/([A-Za-z0-9_-]{1,32}))?$/.exec(path);
    if (options.stations?.vods && vodPath && (request.method === "GET" || request.method === "HEAD")) {
      const built = web !== null && isFile(join(web, "vod.html")) ? readFileSync(join(web, "vod.html"), "utf8") : null;
      if (!built) {
        json(response, 404, { error: "this server was started without its web pages" });
        return;
      }
      const vods = options.stations.vods;
      const one = vodPath[1] ? vods.store.get(vodPath[1]) : undefined;
      const base = watchBase(options.stations.publicBase?.() || `https://${request.headers.host ?? "localhost"}`);
      const view = one ? vodView(one, base, null) : null;
      // A link preview reads the page, not its script: the file's name goes in the head.
      const escape = (text: string): string => text.replace(/[&<>"']/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]!);
      const head = view
        ? `<title>${escape(String(view.name))} · on demand</title>\n    <meta property="og:title" content="${escape(String(view.name))}" />\n    <meta property="og:type" content="${view.kind === "video" ? "video.other" : "music.song"}" />\n    <meta property="og:url" content="${escape(String(view.page))}" />\n    <meta property="og:description" content="On demand: play it from the start, seek anywhere." />`
        : "<title>On demand · nixamp</title>";
      response.writeHead(vodPath[1] && !one ? 404 : 200, {
        "content-type": "text/html; charset=utf-8",
        "cache-control": "no-cache",
        "content-security-policy": adCsp(WATCH_APP_CSP, guideAds()),
        "referrer-policy": "no-referrer",
        "x-content-type-options": "nosniff",
      });
      response.end(request.method === "HEAD" ? undefined : withPageAd(built.replace(/<title>[^<]*<\/title>/, head), guideAds()));
      return;
    }

    // /radio: public, listed stations under readable names, and the artists and
    // albums they play (radio-pages.ts). A station's own paths under its name
    // were answered at the top of this handler.
    // On nixamp.com, live.streaming's artist and album addresses open the same page of the music app.
    if (options.music && /^\/radio\/(artist|album)\//.test(path) && (request.method === "GET" || request.method === "HEAD")) {
      const target = await musicPageFor(options.music.hub, path).catch(() => "");
      if (target) {
        response.writeHead(302, { location: target });
        response.end();
        return;
      }
    }

    if (options.stations && radioPath && (request.method === "GET" || request.method === "HEAD")) {
      const stations = options.stations;
      const named = radioStations(stations.store.list());
      const publicBase = stations.publicBase?.() ?? "";
      const base = onDirectory ? storeSite : radioBaseFor(request, publicBase);
      const nav = radioNav(guideHosts().has(requestHost) ? "/" : "/guide");
      const send = (status: number, page: string, csp = RADIO_CSP): void => {
        const ads = guideAds();
        page = withPageAd(page, ads);
        response.writeHead(status, {
          "content-type": "text/html; charset=utf-8",
          "cache-control": status === 200 ? "public, max-age=60" : "no-cache",
          "content-security-policy": adCsp(csp, ads),
          "referrer-policy": "strict-origin-when-cross-origin",
          "x-content-type-options": "nosniff",
        });
        response.end(request.method === "HEAD" ? undefined : page);
      };
      const shop = stations.label && /^https:\/\//.test(publicBase)
        ? { site: storeSite, buy: radioBuyLink, prices: PRICES }
        : null;
      const peers = stations.peers ? radioPeers(named.keys(), await stations.peers.entries()) : new Map<string, PeerRadio>();
      const stationNames = new Map([...named].map(([slug, one]) => [slug, one.name]));
      if (path === RADIO_BUY) {
        const item = radioBuyQuery(url.searchParams);
        if (!item || !shop) return send(404, renderMissing("thing for sale", base, nav));
        response.writeHead(302, { location: buyLink(storeSite, publicBase, item.kind, item.artist, item.album, item.n), "cache-control": "no-store", "referrer-policy": "no-referrer" });
        response.end();
        return;
      }
      if (path === "/radio" || path === "/radio/") {
        const encrypted = (request.socket as { encrypted?: boolean }).encrypted === true;
        const entries = guideEntries({
          store: stations.store, runner: stations.runner, channels: options.channels!, canControl: false, account: "",
          select: () => null, trackFor: (file) => trackFor(engine, file),
          base: `${encrypted ? "https" : "http"}://${request.headers.host ?? "localhost"}`,
          publicBase, json, readBody, headers: CORS,
        });
        send(200, renderDirectory([
          ...entries.map((one) => ({
            slug: one.radio.split("/radio/").pop() ?? "", name: one.name, description: one.description, state: one.state, kind: one.kind, group: one.group,
            nowPlaying: one.nowPlaying,
          })),
          ...[...peers].map(([slug, one]) => ({ slug, name: one.name, description: "", state: one.state, kind: one.kind, group: one.group, nowPlaying: one.nowPlaying })),
        ], base, nav));
        return;
      }
      // Our label's artists and albums live on nixamp.com/music, where Follow,
      // Buy and the player are; live.streaming is the public mirror and sends
      // them there. Anybody else's keep their page here.
      const onSite = (() => { try { return new URL(storeSite).host === requestHost; } catch { return true; } })();
      const toMusic = (kind: "artist" | "album", slug: string): void => {
        response.writeHead(302, { location: `${storeSite}/radio/${kind}/${encodeURIComponent(slug)}`, "cache-control": "public, max-age=300" });
        response.end();
      };
      const artistPath = RADIO_ARTIST.exec(path);
      if (artistPath) {
        const catalogue = radioCatalogue();
        const artist = catalogue.artists.get(artistPath[1]!);
        if (!artist) return send(404, renderMissing("artist", base, nav));
        if (artist.label && !onSite) return toMusic("artist", artistPath[1]!);
        return send(200, renderArtist(catalogue, artist, { base, nav, shop, site: storeSite, stationNames }));
      }
      const albumPath = RADIO_ALBUM.exec(path);
      if (albumPath) {
        const catalogue = radioCatalogue();
        const album = catalogue.albums.get(albumPath[1]!);
        if (albumPath[2]) {
          const picture = album?.coverFile && stations.cover ? await stations.cover(album.coverFile).catch(() => null) : null;
          if (!picture) {
            json(response, 404, { error: "no cover for that album", code: "not_found" });
            return;
          }
          response.writeHead(200, { "content-type": picture.type, "content-length": picture.bytes.length, "cache-control": "public, max-age=86400", "x-content-type-options": "nosniff" });
          response.end(request.method === "HEAD" ? undefined : picture.bytes);
          return;
        }
        if (!album) return send(404, renderMissing("album", base, nav));
        if (album.label && !onSite) return toMusic("album", albumPath[1]!);
        return send(200, renderAlbum(catalogue, album, { base, nav, shop, site: storeSite, stationNames }));
      }
      const stationPath = RADIO_STATION.exec(path);
      if (stationPath) {
        const slug = stationPath[1]!;
        const station = named.get(slug) ?? (peers.has(slug) ? { name: peers.get(slug)!.name, description: "", video: peers.get(slug)!.kind === "video" } : undefined);
        if (!station) {
          // A station's id finds its name: /radio/5ccqcjn5hg goes to /radio/ai-metal.
          const byId = [...named].find(([, one]) => one.id === slug);
          if (byId) {
            response.writeHead(301, { location: `/radio/${byId[0]}`, "cache-control": "public, max-age=300" });
            response.end();
            return;
          }
          return send(404, renderMissing("station", base, nav));
        }
        const built = web !== null && isFile(join(web, "station.html")) ? readFileSync(join(web, "station.html"), "utf8") : null;
        // The built page carries the one ad; the inline fallback stays as it is.
        const ads = built ? guideAds() : {};
        // Its link preview carries the cover of an album it plays.
        const image = stationImage(radioCatalogue(), slug, base);
        const page = withPageAd(asRadioPage(built ?? STATION_PAGE_HTML, station, slug, base, nav, undefined, image), ads);
        response.writeHead(200, {
          "content-type": "text/html; charset=utf-8",
          "cache-control": "no-cache",
          // The inline page names its styles by hash; the header's is one more.
          "content-security-policy": built ? adCsp(STATION_APP_CSP, ads) : STATION_PAGE_CSP.replace("style-src ", `style-src 'sha256-${sha("sha256").update(NAV_STYLE).digest("base64")}' `),
          "referrer-policy": "strict-origin-when-cross-origin",
          "x-content-type-options": "nosniff",
        });
        response.end(request.method === "HEAD" ? undefined : page);
        return;
      }
    }

    if (options.stations?.vods && (path === "/api/v1/guide/vods" || path.startsWith("/api/v1/guide/vods/"))) {
      const control = await controlOf();
      const session = tokenFrom(request.headers) || url.searchParams.get("session") || "";
      const vods = options.stations.vods;
      const publicBase = (options.stations.publicBase?.() ?? "").replace(/\/+$/, "");
      await handleVods(request, response, path, {
        store: vods.store,
        probe: vods.probe,
        base: watchBase(publicBase || `https://${request.headers.host ?? "localhost"}`),
        canControl: control.allowed,
        member: async () => (session && options.owner ? (await options.owner.accountFor(session)) || "" : ""),
        ownPlay: (item) => (publicBase && listenKey && options.library?.index.item(item) ? `${publicBase}/api/v1/library/items/${item}/play?k=${listenKey}` : ""),
        ownState: (id) => {
          const item = options.library?.index.item(id);
          if (!item || item.state !== "ready") return null;
          const title = item.override.title || item.title || item.rel.split("/").pop()?.replace(/\.[^.]+$/, "") || id;
          return { name: title.slice(0, 160), kind: item.video ? "video" : "audio", duration: item.duration, artist: (item.override.artist || item.artist).slice(0, 160) };
        },
        json,
        readBody,
        throttle: () => {
          const verdict = guard.check(`vods:${callerOf(request.headers, request.socket.remoteAddress, behindProxy)}`, STATION_CONTROL_LIMIT);
          return verdict.ok ? null : { retryAfter: verdict.retryAfter };
        },
      });
      return;
    }

    if (options.stations?.rooms && (path === "/api/v1/guide/rooms" || path.startsWith("/api/v1/guide/rooms/"))) {
      const control = await controlOf();
      const session = tokenFrom(request.headers) || url.searchParams.get("session") || "";
      const rooms = options.stations.rooms;
      await handleRooms(request, response, path, {
        store: rooms.store,
        probe: rooms.probe,
        base: watchBase(options.stations.publicBase?.() || `https://${request.headers.host ?? "localhost"}`),
        canControl: control.allowed,
        member: async () => (session && options.owner ? (await options.owner.accountFor(session)) || "" : ""),
        json,
        readBody,
        throttle: () => {
          const verdict = guard.check(`rooms:${callerOf(request.headers, request.socket.remoteAddress, behindProxy)}`, STATION_CONTROL_LIMIT);
          return verdict.ok ? null : { retryAfter: verdict.retryAfter };
        },
      });
      return;
    }

    if (options.stations && STATION_PAGE.test(path) && (request.method === "GET" || request.method === "HEAD")) {
      // The built page plays in @profullstack/player; the inline one is for a
      // server started without the web build.
      const ads = guideAds();
      const built = web !== null && isFile(join(web, "station.html")) ? withPageAd(readFileSync(join(web, "station.html"), "utf8"), ads) : null;
      response.writeHead(200, {
        "content-type": "text/html; charset=utf-8",
        "cache-control": "no-cache",
        "content-security-policy": built ? adCsp(STATION_APP_CSP, ads) : STATION_PAGE_CSP,
        "referrer-policy": "no-referrer",
        "x-content-type-options": "nosniff",
      });
      response.end(request.method === "HEAD" ? undefined : built ?? STATION_PAGE_HTML);
      return;
    }

    // Past the key gate is past it: the owner, or somebody given the listen
    // link, may read the library; only the owner may change it.
    if (options.library && (path === "/api/v1/library" || path.startsWith("/api/v1/library/"))) {
      const control = await controlOf();
      // Who this is: the owner's account, "owner" for the control key, or
      // any other signed-in nixamp.com account that has the link.
      const session = tokenFrom(request.headers) || url.searchParams.get("session") || "";
      const principal = control.account || (control.allowed ? "owner" : (session && options.owner ? (await options.owner.accountFor(session)) || null : null));
      await handleLibrary(request, response, path, url, {
        index: options.library.index,
        canRead: true,
        canWrite: control.allowed,
        account: control.account,
        principal,
        ...(options.library.listeners ? { listeners: options.library.listeners } : {}),
        probe: options.library.probe,
        retag: options.library.retag,
        sendFile,
        play: (req, res, file) => playFile(req, res, file, url, options),
        json,
        readBody,
      });
      return;
    }

    if (options.importer && path.startsWith("/api/v1/integrations/bittorrented/")) {
      const control = await controlOf();
      await handleHandoffs(request, response, path, {
        importer: options.importer,
        canWrite: control.allowed,
        owner: control.account || "owner",
        json,
        readBody,
      });
      return;
    }

    if (options.music && isMusicPath(path)) {
      const music = options.music;
      const shell = web ? join(web, "music.html") : "";
      if (await handleMusicRoutes(request, response, path, {
        hub: music.hub,
        json,
        readBody,
        page: () => { try { return shell ? readFileSync(shell, "utf8") : null; } catch { return null; } },
        site: storeSite,
        released: music.released,
        ...(music.veo ? { veo: music.veo } : {}),
      })) return;
    }

    if (options.musicStore && options.support && isStorePath(path)) {
      const support = options.support;
      const accounts = options.accounts;
      if (await handleStoreRoutes(request, response, path, url, {
        store: options.musicStore,
        coins: async () => (await support.coins()).coins,
        account: async () => (accounts ? await accounts.whoIs(tokenFrom(request.headers)) : null),
        json,
        readBody,
      })) return;
    }

    if (options.musicLicenses && isLicensePath(path)) {
      const accounts = options.accounts;
      if (await handleLicenseRoutes(request, response, path, url, {
        licenses: options.musicLicenses,
        account: async () => (accounts ? await accounts.whoIs(tokenFrom(request.headers)) : null),
        json,
        readBody,
        serverId,
        site: (process.env["NIXAMP_SITE"] || "https://nixamp.com").replace(/\/+$/, ""),
      })) return;
    }

    if (options.stations?.label && (isPublicLabelPath(path) || path === "/api/v1/stations/label" || path.startsWith("/api/v1/stations/label/"))) {
      const control = isPublicLabelPath(path) ? { allowed: false } : await controlOf();
      if (await handleLabelRoutes(request, response, path, {
        label: options.stations.label, canControl: control.allowed, json, readBody,
        grants: storeGrants, site: storeSite, publicBase: options.stations.publicBase?.() ?? "", ads: guideAds(),
      })) return;
    }

    if (options.stations && (isPublicSharePath(path) || path.startsWith("/api/v1/stations/ai") || path.startsWith("/api/v1/stations/shares") || path.startsWith("/api/v1/stations/holders"))) {
      const control = await controlOf();
      // Behind a proxy we trust (dev2 for live.streaming), the sharer is the
      // first address it names; otherwise whoever connected.
      const socket = (request.socket.remoteAddress ?? "").replace(/^::ffff:/, "");
      const trusted = behindProxy || (options.stations.trustedProxies?.().has(socket) ?? false);
      const caller = callerOf(request.headers, socket, trusted);
      const handled = await handleAiRoutes(request, response, path, url, {
        ...(options.stations.ai ? { ai: options.stations.ai } : {}),
        ...(options.stations.shares ? { shares: options.stations.shares } : {}),
        ...(options.stations.holders ? { holders: options.stations.holders } : {}),
        canControl: control.allowed,
        caller,
        json,
        readBody,
        headers: CORS,
        throttle: () => {
          const verdict = guard.check(`shares:${caller}`, STATION_CONTROL_LIMIT);
          return verdict.ok ? null : { retryAfter: verdict.retryAfter };
        },
      });
      if (handled) return;
    }

    if (options.stations && (path === "/api/v1/stations" || path.startsWith("/api/v1/stations/"))) {
      const control = await controlOf();
      const encrypted = (request.socket as { encrypted?: boolean }).encrypted === true;
      const forwarded = behindProxy ? String(request.headers["x-forwarded-proto"] ?? "").split(",")[0]?.trim() : "";
      const scheme = forwarded === "https" || encrypted ? "https" : "http";
      await handleStations(request, response, path, url, {
        store: options.stations.store,
        runner: options.stations.runner,
        channels: options.channels!,
        canControl: control.allowed,
        account: control.account,
        select: (body) => {
          // A playlist of the owner's, as the station's files: its ready items, in its order.
          if (typeof (body as { playlist?: unknown }).playlist === "string" && options.library?.listeners) {
            const owner = control.account || "owner";
            let list;
            try {
              list = options.library.listeners.playlist(owner, (body as { playlist: string }).playlist);
            } catch (error) {
              throw new StationError("playlist_not_found", (error as Error).message, 404);
            }
            const files = list.items.map((id) => options.library!.index.item(id)).filter((one) => one?.state === "ready").map((one) => one!.path);
            if (files.length === 0) throw new StationError("empty_playlist", "none of that playlist's items are ready to play");
            return files;
          }
          return selectStationFiles(engine, body);
        },
        trackFor: (file) => trackFor(engine, file),
        ...(options.library ? { isVideo: (file: string) => options.library!.index.itemAt(file)?.video === true } : {}),
        ...(options.library ? { durationOf: (file: string) => options.library!.index.itemAt(file)?.duration ?? 0 } : {}),
        base: `${scheme}://${request.headers.host ?? "localhost"}`,
        publicBase: options.stations.publicBase?.() ?? "",
        json,
        readBody,
        headers: CORS,
        ...(options.stations.usage ? { usage: options.stations.usage } : {}),
        ...(options.stations.limits ? { limits: options.stations.limits } : {}),
        ...(options.stations.auto ? { auto: options.stations.auto } : {}),
        ...(options.stations.facts ? { facts: options.stations.facts } : {}),
        ...(options.stations.cover ? { cover: options.stations.cover } : {}),
        ...(options.stations.lyrics ? { lyrics: options.stations.lyrics } : {}),
        ...(options.stations.lyricsPublisher ? { lyricsPublisher: options.stations.lyricsPublisher } : {}),
        passes: listenPasses,
        ...(options.stations.peers ? { peers: () => options.stations!.peers!.entries() } : {}),
        ...(options.stations.rooms || options.stations.vods ? {
          rooms: async () => {
            const base = watchBase(options.stations!.publicBase?.() || `${scheme}://${request.headers.host ?? "localhost"}`);
            const { rooms, vods } = options.stations!;
            const [live, onDemand] = await Promise.all([
              rooms ? roomEntries(rooms.store, rooms.probe, base) : [],
              vods ? vodEntries(vods.store, vods.probe, base) : [],
            ]);
            return [...live, ...onDemand];
          },
        } : {}),
        throttle: () => {
          const verdict = guard.check(`stations:${control.account || callerOf(request.headers, request.socket.remoteAddress, behindProxy)}`, STATION_CONTROL_LIMIT);
          return verdict.ok ? null : { retryAfter: verdict.retryAfter };
        },
      });
      return;
    }

    if (path === "/api/health") {
      json(response, 200, { name: "nixamp", version: options.version, media: options.media });
      return;
    }

    if (path === "/reset-password" && options.accounts?.passwordResets) {
      if (request.method !== "GET" && request.method !== "HEAD") {
        json(response, 405, { error: "GET only" }); return;
      }
      const nonce = randomBytes(18).toString("base64");
      response.writeHead(200, {
        "content-type": "text/html; charset=utf-8", "cache-control": "no-store",
        "referrer-policy": "no-referrer", "x-robots-tag": "noindex, nofollow",
        "content-security-policy": `default-src 'none'; script-src 'nonce-${nonce}'; style-src 'unsafe-inline'; connect-src 'self'; base-uri 'none'; frame-ancestors 'none'; form-action 'self'`,
      });
      response.end(request.method === "HEAD" ? undefined : passwordResetPage(accountSite, nonce));
      return;
    }

    // --- accounts ---------------------------------------------------------
    //
    // Before the share-key check, because signing in is how somebody without a
    // key becomes somebody with one. The API is versioned and namespaced the
    // way the rest of the fleet's is.
    if (path.startsWith("/api/v1/auth/") && options.accounts) {
      const accounts = options.accounts;
      const secure = options.secureCookies ?? false;

      if (path === "/api/v1/auth/password-reset/request" || path === "/api/v1/auth/password-reset/confirm") {
        if (request.method !== "POST") { json(response, 405, { error: "POST only" }); return; }
        const confirming = path.endsWith("/confirm");
        if (!accounts.passwordResets || (!confirming && !options.resetMail)) {
          json(response, 503, { error: "Password reset email is temporarily unavailable. Please try again later." }); return;
        }
        const who = callerOf(request.headers, request.socket.remoteAddress, behindProxy);
        const limit = guard.check(`password-reset:${confirming ? "confirm" : "request"}:${who}`, { allowed: confirming ? 15 : 10, windowMs: 15 * 60_000 });
        if (!limit.ok) {
          response.setHeader("retry-after", String(limit.retryAfter));
          json(response, 429, { error: "Too many attempts. Please try again in 15 minutes." }); return;
        }
        try {
          const input: unknown = JSON.parse(await readBody(request, 4096));
          if (!input || typeof input !== "object" || Array.isArray(input)) {
            json(response, 400, { error: "Provide a JSON object." }); return;
          }
          const body = input as Record<string, unknown>;
          if (confirming) {
            const error = await accounts.passwordResets.confirm(body["token"], body["password"]);
            if (error) { json(response, error === RESET_INVALID ? 400 : 422, { error }); return; }
            response.setHeader("set-cookie", clearedCookie());
            json(response, 200, { message: "Your password has been changed. Sign in with your new password." });
          } else {
            const email = typeof body["email"] === "string" ? body["email"].trim().toLowerCase() : "";
            if (email.length > 320 || !/^[^@\s]+@[^@\s.]+\.[^@\s]+$/.test(email)) {
              json(response, 422, { error: "Enter a valid email address." }); return;
            }
            // Count every address, including nonexistent ones, identically.
            const address = guard.check(`password-reset:email:${email}`, { allowed: 3, windowMs: 15 * 60_000 });
            if (address.ok) await accounts.passwordResets.request(email, accountSite, options.resetMail!);
            json(response, 200, { message: RESET_MESSAGE });
          }
        } catch (error) {
          const malformed = error instanceof SyntaxError || (error as Error).message === "body too large";
          json(response, malformed ? 400 : 503, { error: malformed ? "Provide a valid JSON request." : "Password reset is temporarily unavailable. Please try again." });
        }
        return;
      }

      if (path === "/api/v1/auth/me") {
        const token = tokenFrom(request.headers);
        const who = await accounts.whoIs(token);
        if (who === null) {
          json(response, 401, { error: "not signed in" });
          return;
        }
        // An app acting for the person says so, with what it was allowed:
        // a server asking "is this my owner?" must not take a token handed to
        // some other app as the owner themselves.
        const grant = options.authServer ? await options.authServer.grantFor(token) : null;
        json(response, 200, { account: who, ...(grant ? { grant: { clientId: grant.clientId, scope: grant.scope } } : {}) });
        return;
      }

      /**
       * The session itself, for the page to carry to a server on another
       * origin. The cookie is this host's and stays here; a server elsewhere
       * can only be shown the session in the query, the way it is shown the
       * key. Only a signed-in caller gets one, and it is the token they
       * already hold -- nothing new is minted.
       */
      if (path === "/api/v1/auth/token") {
        const token = tokenFrom(request.headers);
        const who = await accounts.whoIs(token);
        if (who === null) {
          json(response, 401, { error: "not signed in" });
          return;
        }
        json(response, 200, { token });
        return;
      }

      if (path === "/api/v1/auth/logout") {
        // The cookie going is what the browser notices; the token going is
        // what makes it stop working on a machine you no longer have.
        await accounts.endSession(tokenFrom(request.headers));
        response.writeHead(200, {
          ...CORS,
          "content-type": "application/json; charset=utf-8",
          "set-cookie": clearedCookie(),
        });
        response.end(JSON.stringify({ ok: true }));
        return;
      }

      // What this deployment will accept, so the CLI offers the ways in that
      // exist here rather than a menu built from what it hopes is configured.
      if (path === "/api/v1/auth/providers") {
        json(response, 200, {
          password: true,
          device: options.signIn !== undefined,
          providers: options.signIn?.offered ?? [],
        });
        return;
      }

      // --- the device grant, for a terminal with no browser ---------------
      if (path.startsWith("/api/v1/auth/device") && options.signIn) {
        const signIn = options.signIn;

        // A terminal asks for a code to show, and starts polling.
        if (path === "/api/v1/auth/device/code" && request.method === "POST") {
          const grant = signIn.device.start();
          const where = `${signIn.site}/api/v1/auth/device`;
          json(response, 200, {
            device_code: grant.deviceCode,
            user_code: grant.userCode,
            verification_uri: where,
            // The pre-filled link is what makes this one click on a phone.
            verification_uri_complete: `${where}?code=${encodeURIComponent(grant.userCode)}`,
            expires_in: Math.round((grant.expiresAt - Date.now()) / 1000),
            interval: signIn.device.interval,
          });
          return;
        }

        // ... and asks, at that interval, whether anybody has approved it yet.
        if (path === "/api/v1/auth/device/token" && request.method === "POST") {
          let body: { device_code?: unknown };
          try {
            body = JSON.parse(await readBody(request)) as typeof body;
          } catch {
            json(response, 400, { error: "bad JSON" });
            return;
          }
          const status = signIn.device.poll(String(body.device_code ?? ""));
          if (status.status === "ok") {
            json(response, 200, { token: status.token, email: status.email });
            return;
          }
          // The names are RFC 8628's, because that is what a client waiting on
          // a device grant already knows how to read.
          const named = {
            pending: "authorization_pending",
            slow_down: "slow_down",
            expired: "expired_token",
            denied: "access_denied",
          } as const;
          json(response, 400, { error: named[status.status] });
          return;
        }

        // The page somebody opens on a device that has a keyboard.
        if (path === "/api/v1/auth/device" && (request.method === "GET" || request.method === "HEAD")) {
          const who = await accounts.whoIs(tokenFrom(request.headers));
          html(response, 200, devicePage(signIn, url.searchParams.get("code") ?? "", who?.email ?? ""));
          return;
        }

        if (path === "/api/v1/auth/device" && request.method === "POST") {
          const form = new URLSearchParams(await readBody(request));
          const code = form.get("code") ?? "";
          const grant = signIn.device.find(code);
          if (grant === null) {
            html(response, 404, signInFailedPage("That code has expired or was already used. Ask your terminal for another."));
            return;
          }

          // Empty means "approve as the account this browser is already signed
          // in as"; anything else names a provider to go and ask.
          //
          // A form on somebody else's site posting here is what would make
          // this dangerous, and is what SameSite=Lax on the session cookie
          // prevents: a cross-site POST arrives with no cookie, so it is
          // nobody, so it approves nothing.
          const chosen = form.get("with") ?? "";
          if (chosen === "") {
            const who = await accounts.whoIs(tokenFrom(request.headers));
            if (who === null) {
              html(response, 401, signInFailedPage("Sign in first, then approve the terminal."));
              return;
            }
            const token = await accounts.sessionFor(who);
            if (!token || !signIn.device.approve(grant.userCode, { token, email: who.email })) {
              html(response, 500, signInFailedPage("Could not start a session for that terminal."));
              return;
            }
            html(response, 200, deviceDonePage(who.email));
            return;
          }

          const provider = signIn.provider(chosen);
          if (provider === null) {
            html(response, 404, signInFailedPage("This nixamp cannot sign you in with that."));
            return;
          }
          // The user code rides along in the state, so the callback knows it
          // is approving a terminal rather than signing this browser in.
          response.writeHead(302, { location: signIn.begin(provider, grant.userCode) });
          response.end();
          return;
        }

        json(response, 404, { error: "no such endpoint" });
        return;
      }

      // --- tokens a person made on purpose --------------------------------
      if (path === "/api/v1/auth/tokens" || path.startsWith("/api/v1/auth/tokens/")) {
        const who = await accounts.whoIs(tokenFrom(request.headers));
        if (who === null) {
          json(response, 401, { error: "not signed in" });
          return;
        }

        if (path === "/api/v1/auth/tokens" && request.method === "GET") {
          json(response, 200, { tokens: await accounts.listTokens(who.id, "cli") });
          return;
        }

        if (path === "/api/v1/auth/tokens" && request.method === "POST") {
          let body: { name?: unknown };
          try {
            body = JSON.parse(await readBody(request)) as typeof body;
          } catch {
            json(response, 400, { error: "bad JSON" });
            return;
          }
          const name = typeof body.name === "string" ? body.name.slice(0, 80) : "";
          const made = await accounts.mintCliToken(who, name);
          if (made === null) {
            json(response, 501, { error: "this nixamp does not keep tokens" });
            return;
          }
          // The whole token is in this answer and in no other: it is not
          // stored, so there is nowhere to show it again from.
          json(response, 201, { token: made.token, id: made.id, name: made.name });
          return;
        }

        const id = path.slice("/api/v1/auth/tokens/".length);
        if (id && request.method === "DELETE") {
          const gone = await accounts.revokeToken(who.id, id);
          json(response, gone ? 200 : 404, gone ? { ok: true } : { error: "no such token" });
          return;
        }

        json(response, 405, { error: "GET, POST or DELETE" });
        return;
      }

      const signingUp = path === "/api/v1/auth/signup";
      if (!signingUp && path !== "/api/v1/auth/login") {
        json(response, 404, { error: "no such endpoint" });
        return;
      }
      if (request.method !== "POST") {
        json(response, 405, { error: "POST only" });
        return;
      }

      let body: { email?: unknown; password?: unknown };
      try {
        body = JSON.parse(await readBody(request)) as typeof body;
      } catch {
        json(response, 400, { error: "bad JSON" });
        return;
      }

      // A password is the one secret here small enough to guess, so the
      // attempts are counted per caller and per address being tried: one
      // machine working through a word list and a thousand machines trying one
      // address are the same attack, and each is stopped by its own counter.
      const who = callerOf(request.headers, request.socket.remoteAddress, behindProxy);
      const target = typeof body.email === "string" ? body.email.toLowerCase().slice(0, 200) : "";
      const buckets = [`signin:${who}`, `signin:${target}`];
      for (const bucket of buckets) {
        const verdict = guard.check(bucket, SIGN_IN_LIMIT);
        if (!verdict.ok) {
          response.writeHead(429, {
            ...CORS,
            "content-type": "application/json; charset=utf-8",
            "retry-after": String(verdict.retryAfter),
          });
          response.end(JSON.stringify({ error: "too many attempts; try again later" }));
          return;
        }
      }

      const result = signingUp
        ? await accounts.signUp(body.email, body.password)
        : await accounts.signIn(body.email, body.password);

      // A handle asked for at sign-up, or one nobody has to think about. Never
      // derived from the address: turning anthony@… into "anthony" is the leak
      // this whole idea exists to avoid, and it is one nobody would notice
      // until it was already in a directory listing.
      if (signingUp && result.ok && result.account && options.handles) {
        const asked = (body as { handle?: unknown }).handle;
        const claimed = await options.handles.claim(result.account.id, asked);
        if (claimed.error) {
          await options.handles.claim(result.account.id, anonymousHandle((size) => randomBytes(size)));
        }
      }

      // Getting it right costs nothing: the counters only exist to stop people
      // who keep getting it wrong.
      if (result.ok) for (const bucket of buckets) guard.forget(bucket);

      if (!result.ok) {
        // 409 for an address that is taken, 401 for credentials that are not.
        json(response, signingUp ? 409 : 401, { error: result.error });
        return;
      }

      // A password sign-in ends in the same revocable token an OAuth one does,
      // falling back to the module's JWT where there is no storage to keep one
      // in. Every way in should be a session that can be listed and ended.
      const token = result.account
        ? await accounts.sessionFor(result.account, result.token)
        : result.token;

      // The token goes back in the body for the CLI and the desktop app, and
      // as a cookie for the browser, which then needs to know nothing about it.
      response.writeHead(200, {
        ...CORS,
        "content-type": "application/json; charset=utf-8",
        "set-cookie": sessionCookie(token, secure),
      });
      response.end(JSON.stringify({ account: result.account, token }));
      return;
    }

    // --- the name other people see ---------------------------------------
    //
    // Separate from the address on purpose. The address is a credential and
    // a way to reach somebody; publishing it in a directory listing or an
    // invite would be publishing what they log in with.
    /**
     * A trollbox line, read aloud to whoever is on the phone in that room.
     * The room's code comes from the directory (one per channel, one for
     * the server's own stream), the voice from the author's account or
     * OpenProfile. Never awaited by the poster: the phone is a side effect
     * of the chat, and a slow Telnyx must not slow the box.
     */
    const readOnThePhone = (where: { server: string; channel: string }, authorId: string, handle: string, body: string): void => {
      const partyLine = options.partyLine;
      const listing = (options.directory?.list() ?? []).find((one) => {
        try {
          return new URL(one.url).origin === where.server;
        } catch {
          return false;
        }
      });
      if (!partyLine || !listing) return;
      const code = where.channel === "live" || where.channel === "main" ? listing.code : listing.channelCodes[where.channel] ?? "";
      if (!code || !partyLine.hasCallers(code)) return;
      void (async () => {
        const persona = (await options.handles?.persona(authorId)) ?? { handle, voice: "", profile: "" };
        const profile = persona.profile && options.profiles ? await options.profiles.voiceOf(persona.profile) : null;
        const pools = options.voices ? await options.voices.pools() : { provider: "kokoro" as const, female: [], male: [] };
        const spoken = spokenVoiceFor({ userId: authorId, voice: persona.voice, profile }, pools);
        await partyLine.say(code, spokenLine(handle, body), spoken.voice, spoken.settings);
      })().catch(() => undefined);
    };

    /*
     * The voices a line can be read in, for whoever wants to pick one by
     * id: which provider, and the women's and the men's pools. Signed in,
     * like the profile it feeds.
     */
    if (path === "/api/v1/voices" && options.accounts) {
      const who = await options.accounts.whoIs(tokenFrom(request.headers));
      if (who === null) {
        json(response, 401, { error: "not signed in" });
        return;
      }
      const pools = options.voices ? await options.voices.pools() : { provider: "kokoro" as const, female: [], male: [] };
      json(response, 200, { provider: pools.provider, female: pools.female, male: pools.male });
      return;
    }

    /*
     * The trollbox: the chat for one live room, keyed by the server and
     * the channel so every viewer of a stream is in the same box whichever
     * page they came from. Reading is open; a line needs a nixamp.com
     * account and is signed with its public handle; and once sent it is
     * public record, so there is no DELETE here for anybody.
     */
    if ((path === "/api/v1/trollbox" || path.startsWith("/api/v1/trollbox/")) && options.trollbox && options.accounts) {
      const trollbox = options.trollbox;
      const child = path.startsWith("/api/v1/trollbox/") ? path.slice("/api/v1/trollbox/".length) : "";
      // Nothing lives under a line's id: a sent line is public record, so
      // there is no line to address, whatever the method.
      if (child !== "") {
        json(response, 404, { error: "no such endpoint" });
        return;
      }
      try {
        if (request.method === "GET") {
          const where = roomFor(url.searchParams.get("server"), url.searchParams.get("channel"));
          if (!where) {
            json(response, 400, { error: "a room is a server address and a channel" });
            return;
          }
          const who = await options.accounts.whoIs(tokenFrom(request.headers));
          const lines = await trollbox.messages(where.room, url.searchParams.get("after") ?? undefined);
          json(response, 200, {
            room: where.room,
            you: who ? ((await options.handles?.of(who.id)) || fallbackHandle(who.id)) : "",
            messages: lines.map((one) => ({ id: one.id, handle: one.handle, body: one.body, createdAt: one.createdAt })),
          });
          return;
        }
        if (request.method === "POST") {
          const who = await options.accounts.whoIs(tokenFrom(request.headers));
          if (who === null) {
            json(response, 401, { error: "sign in to nixamp.com to chat" });
            return;
          }
          let body: { server?: unknown; channel?: unknown; body?: unknown } = {};
          try {
            body = JSON.parse(await readBody(request)) as typeof body;
          } catch {
            json(response, 400, { error: "bad JSON" });
            return;
          }
          const where = roomFor(body.server, body.channel);
          if (!where) {
            json(response, 400, { error: "a room is a server address and a channel" });
            return;
          }
          const handle = (await options.handles?.of(who.id)) || fallbackHandle(who.id);
          const line = await trollbox.post(where, who.id, handle, body.body);
          readOnThePhone(where, who.id, line.handle, line.body);
          json(response, 201, { message: { id: line.id, handle: line.handle, body: line.body, createdAt: line.createdAt } });
          return;
        }
        json(response, 405, { error: "GET or POST" });
      } catch (error) {
        if (error instanceof TrollboxError) json(response, error.status, { error: error.message });
        else throw error;
      }
      return;
    }

    if (path === "/api/v1/speech/shared" && request.method === "POST") {
      try {
        const service = options.sharedTranslations;
        const who = await options.accounts?.whoIs(tokenFrom(request.headers));
        if (!who) throw new SpeechError("Sign in to translate live audio.", 401);
        if (!service) throw new SpeechError("Shared translation is unavailable.", 503);
        const caller = callerOf(request.headers, request.socket.remoteAddress, options.behindProxy ?? false);
        if (!guard.check(`shared-join:${who.id}`, { allowed: 10, windowMs: 60_000 }).ok || !guard.check(`shared-ip:${caller}`, { allowed: 30, windowMs: 60_000 }).ok) throw new SpeechError("Too many live translation joins.", 429);
        if (!request.headers["content-type"]?.startsWith("application/json")) throw new SpeechError("Send a live source as JSON.", 415);
        let body: { source?: unknown; language?: unknown };
        try { body = JSON.parse(await readBody(request, 4096)); } catch { throw new SpeechError("Choose a live source and language.", 400); }
        if (typeof body?.source !== "string" || typeof body.language !== "string") throw new SpeechError("Choose a live source and language.", 400);
        const queued: SharedEvent[] = [];
        let finished = false;
        const send = (event: SharedEvent): void => {
          if (!response.headersSent) { queued.push(event); return; }
          if (response.destroyed || response.writableLength > 512_000) throw new Error("slow listener");
          response.write(`data: ${JSON.stringify(event)}\n\n`);
        };
        const leave = await service.join(body.source, body.language, who.id, send, () => { finished = true; if (response.headersSent) response.end(); });
        if (response.destroyed) { leave(); return; }
        response.writeHead(200, { ...CORS, "content-type": "text/event-stream", "cache-control": "no-store", "x-accel-buffering": "no" });
        for (const event of queued) send(event);
        if (finished) { leave(); response.end(); return; }
        const heartbeat = setInterval(() => { if (!response.destroyed) response.write(": listening\n\n"); }, 10_000);
        response.once("close", () => { clearInterval(heartbeat); leave(); });
      } catch (error) {
        if (response.headersSent) response.end();
        else json(response, error instanceof SpeechError ? error.status : 502, { error: error instanceof SpeechError ? error.message : "Shared translation is unavailable." });
      }
      return;
    }

    if (path === "/api/v1/translation-passes" || path.startsWith("/api/v1/translation-passes/")) {
      response.setHeader("cache-control", "no-store");
      try {
        const caller = callerOf(request.headers, request.socket.remoteAddress, options.behindProxy ?? false);
        if (!guard.check(`translation-pass-ip:${caller}`, { allowed: 120, windowMs: 60_000 }).ok) throw new SpeechError("Too many purchase requests.", 429);
        const service = options.translationPasses;
        if (!service || !options.accounts) {
          if (path === "/api/v1/translation-passes" && request.method === "GET") json(response, 200, { required: false, available: false, balanceMicros: 0, expires: null, plans: [], coins: [], orders: [] });
          else json(response, 503, { error: "Translation checkout is unavailable." });
          return;
        }
        const who = await options.accounts.whoIs(tokenFrom(request.headers));
        if (path === "/api/v1/translation-passes" && request.method === "GET") {
          json(response, 200, await service.access(who?.id)); return;
        }
        if (!who) throw new SpeechError("Sign in to use translated audio.", 401);
        if (!guard.check(`translation-pass-account:${who.id}`, { allowed: 30, windowMs: 60_000 }).ok) throw new SpeechError("Too many purchase requests.", 429);
        if (path === "/api/v1/translation-passes/session" && request.method === "POST") {
          if (!request.headers["content-type"]?.startsWith("application/json")) throw new SpeechError("Send a panel session as JSON.", 415);
          let body: { resource?: unknown };
          try { body = JSON.parse(await readBody(request, 1024)); } catch { throw new SpeechError("Choose a panel session.", 400); }
          if (typeof body?.resource !== "string") throw new SpeechError("Choose a panel session.", 400);
          await service.begin(who.id, body.resource);
          json(response, 200, await service.access(who.id)); return;
        }
        if (path === "/api/v1/translation-passes/checkout" && request.method === "POST") {
          if (!request.headers["content-type"]?.startsWith("application/json")) throw new SpeechError("Send a purchase as JSON.", 415);
          if (!guard.check(`translation-checkout-ip:${caller}`, { allowed: 10, windowMs: 3600_000 }).ok) throw new SpeechError("Too many new checkouts. Resume your pending purchase.", 429);
          let body: Record<string, unknown>;
          try { body = JSON.parse(await readBody(request, 1024)); } catch { throw new SpeechError("Choose a translation pass.", 400); }
          if (!body || typeof body !== "object" || typeof body["plan"] !== "string" || typeof body["coin"] !== "string" || typeof body["requestKey"] !== "string") throw new SpeechError("Choose a translation pass and payment currency.", 400);
          json(response, 200, await service.checkout(who.id, body["plan"], body["coin"], body["requestKey"])); return;
        }
        const order = path.match(/^\/api\/v1\/translation-passes\/orders\/([^/]+)$/);
        if (order && request.method === "GET") {
          json(response, 200, await service.check(who.id, order[1]!)); return;
        }
        json(response, 405, { error: "GET your balance or purchase status; POST a checkout." });
      } catch (error) {
        json(response, error instanceof SpeechError ? error.status : 502, { error: error instanceof SpeechError ? error.message : "Translation purchases are temporarily unavailable." });
      }
      return;
    }

    /*
     * Speech to text: a WAV in, the words out. Signed in only -- the ear
     * costs CPU and a trollbox line needs a name anyway -- and, given a
     * room, the words go straight into that room's trollbox as a line by
     * whoever spoke them. The page, the CLI and the MCP tools all come here.
     */
    if (["/api/v1/speech/voices", "/api/v1/speech/grant", "/api/v1/speech/synthesize", "/api/v1/speech/speakers"].includes(path) && options.accounts) {
      const controller = new AbortController();
      response.once("close", () => controller.abort());
      try {
        const caller = callerOf(request.headers, request.socket.remoteAddress, options.behindProxy ?? false);
        if (!guard.check(`voice-ip:${caller}`, { allowed: 600, windowMs: 60_000 }).ok) { json(response, 429, { error: "too many voice requests" }); return; }
        const service = options.liveVoice;
        if (!service?.available()) { json(response, 503, { error: "translated audio needs ELEVENLABS_API_KEY on the account server" }); return; }
        if (path.endsWith("/synthesize") && request.method === "POST") {
          let body: VoiceRequest;
          try { body = JSON.parse(await readBody(request, 4096)) as VoiceRequest; }
          catch { json(response, 400, { error: "send a caption as JSON" }); return; }
          if (!body || typeof body !== "object") { json(response, 400, { error: "send a caption as JSON" }); return; }
          const by = await service.authorize(tokenFrom(request.headers), body.channel ?? "", typeof body.text === "string" ? body.text.length : 0);
          await voiceResponse(response, await service.stream(body, by, controller.signal));
        } else {
          const who = await options.accounts.whoIs(tokenFrom(request.headers));
          if (!who) { json(response, 401, { error: "sign in to use translated audio" }); return; }
          if (path.endsWith("/voices") && request.method === "GET") {
            json(response, 200, { voices: await service.voices(), model: LIVE_VOICE_MODEL, languages: [...LIVE_VOICE_LANGUAGES] });
          } else if (path.endsWith("/speakers") && request.method === "POST") {
            if (!request.headers["content-type"]?.startsWith("audio/wav")) { json(response, 415, { error: "send a mono 16 kHz WAV" }); return; }
            const bytes = await readBytes(request, 484_000);
            const resource = request.headers["x-nixamp-translation-session"];
            if (resource !== undefined && (typeof resource !== "string" || !/^[\w-]{1,80}$/.test(resource))) throw new SpeechError("Choose a panel session.", 400);
            json(response, 200, await service.hear(bytes, who.id, controller.signal, undefined, resource));
          } else if (path.endsWith("/grant") && request.method === "POST") {
            if (!request.headers["content-type"]?.startsWith("application/json")) { json(response, 415, { error: "send JSON" }); return; }
            let body: { channel?: unknown };
            try { body = JSON.parse(await readBody(request, 1024)) as typeof body; }
            catch { json(response, 400, { error: "send a channel as JSON" }); return; }
            json(response, 200, await service.grant(who.id, typeof body?.channel === "string" ? body.channel : ""));
          } else json(response, 405, { error: "GET voices or POST an audio request" });
        }
      } catch (error) {
        if (response.headersSent) response.destroy();
        else json(response, error instanceof SpeechError ? error.status : 502, { error: error instanceof SpeechError ? error.message : "translated audio is unavailable" });
      }
      return;
    }

    if (path === "/api/v1/speech/transcribe" && options.speech && options.accounts) {
      const speech = options.speech;
      try {
        if (request.method !== "POST") {
          json(response, 405, { error: "POST a WAV" });
          return;
        }
        const who = await options.accounts.whoIs(tokenFrom(request.headers));
        if (who === null) {
          json(response, 401, { error: "sign in to nixamp.com to dictate" });
          return;
        }
        const bytes = await readBytes(request, SPEECH_BYTES);
        if (!isWav(bytes)) {
          json(response, 415, { error: "send a WAV: 16-bit PCM, mono, 16 kHz is ideal" });
          return;
        }
        // A room named is a line posted, by the same rules as typing it.
        const roomAsked = url.searchParams.has("server") || url.searchParams.has("channel");
        const where = roomAsked ? roomFor(url.searchParams.get("server"), url.searchParams.get("channel")) : null;
        if (roomAsked && !where) {
          json(response, 400, { error: "a room is a server address and a channel" });
          return;
        }
        const heard = await speech.transcribe(bytes, {
          language: languageOf(url.searchParams.get("language")),
          // Pieces with their timing, for a whole file being written down.
          timestamps: url.searchParams.get("timestamps") === "1",
          by: who.id,
          ...(url.searchParams.get("live") === "1" ? { deadline: Date.now() + 10_000 } : {}),
        });
        // The language travels back: as told, or as the ear guessed it, so
        // a captioner can say it next time and a transcript can be kept as it.
        const said = {
          text: heard.text, seconds: heard.seconds, model: speech.model,
          ...(heard.language ? { language: heard.language } : {}),
          ...(heard.segments ? { segments: heard.segments } : {}),
        };
        if (where && options.trollbox && heard.text !== "") {
          const handle = (await options.handles?.of(who.id)) || fallbackHandle(who.id);
          const line = await options.trollbox.post(where, who.id, handle, heard.text);
          readOnThePhone(where, who.id, line.handle, line.body);
          json(response, 201, {
            ...said,
            message: { id: line.id, handle: line.handle, body: line.body, createdAt: line.createdAt },
          });
          return;
        }
        json(response, 200, said);
      } catch (error) {
        if (error instanceof SpeechError || error instanceof TrollboxError) json(response, error.status, { error: error.message });
        else throw error;
      }
      return;
    }

    /*
     * One address per file: /hash/<sha256>, the record of the bytes (see
     * media.ts). As a page for a person, as an OpenFile file object or plain
     * JSON for a program, and its transcript as subtitles. Reading is open:
     * the id is the hash of the bytes, and whoever has it has the file.
     * Keeping (PUT on the API) is signed in, like the ear.
     */
    if (path === "/.well-known/openfile.json" && options.records && options.site) {
      json(response, 200, openFileListing(await options.records.recent(), options.site));
      return;
    }
    if ((path.startsWith("/hash/") || path === "/api/v1/media" || path.startsWith("/api/v1/media/")) && options.records && options.transcripts && options.site) {
      const media = options.records;
      const store = options.transcripts;
      const site = options.site;
      const viaApi = path.startsWith("/api/v1/media");
      const tail = viaApi ? path.slice("/api/v1/media".length).replace(/^\//, "") : path.slice("/hash/".length);
      const parts = /^([^/.]+)(?:\.(openfile\.json|json|srt|vtt|txt))?\/?$/.exec(tail);
      if (!parts) {
        json(response, 404, { error: "a file is /hash/<sha256>, with .json, .openfile.json, .srt, .vtt or .txt after it" });
        return;
      }
      let named = "";
      try {
        named = decodeURIComponent(parts[1] as string);
      } catch {
        json(response, 400, { error: "bad id" });
        return;
      }
      const format = parts[2] ?? "";
      try {
        if (request.method === "PUT" && viaApi) {
          const who = await options.accounts?.whoIs(tokenFrom(request.headers));
          if (!who) {
            json(response, 401, { error: "sign in to nixamp.com to keep a file's record" });
            return;
          }
          const id = mediaId(named);
          if (!id) {
            json(response, 400, { error: "the id is the file's SHA-256, hex" });
            return;
          }
          let body: { name?: unknown; size?: unknown; contentType?: unknown; updated?: unknown; facts?: unknown; holder?: unknown } = {};
          try {
            body = JSON.parse(await readBody(request, 256 * 1024)) as typeof body;
          } catch {
            json(response, 400, { error: "bad JSON" });
            return;
          }
          const saved = await media.save({
            id,
            ...(typeof body.name === "string" ? { name: body.name.trim() } : {}),
            ...(typeof body.size === "number" && body.size >= 0 ? { size: Math.floor(body.size) } : {}),
            ...(typeof body.contentType === "string" ? { contentType: body.contentType.trim().slice(0, 100) } : {}),
            ...(typeof body.updated === "string" && !Number.isNaN(Date.parse(body.updated)) ? { updated: new Date(body.updated).toISOString() } : {}),
            facts: factsFromRequest(body.facts),
            holder: holderFrom(body.holder),
            by: who.id,
          });
          if (!saved) {
            json(response, 500, { error: "the record was not kept" });
            return;
          }
          json(response, 200, openFileOf(saved, site, saved.facts.fingerprint ? await store.languages(transcriptIdOf(saved.facts.fingerprint)) : []));
          return;
        }
        if (request.method !== "GET") {
          json(response, 405, { error: "GET the record, or PUT what you know" });
          return;
        }
        // By the SHA-256, by the transcript store's fingerprint, or by a transcript id.
        let record: MediaRecord | null = null;
        const hex = mediaId(named);
        if (hex) record = await media.get(hex);
        if (!record && /^file:v1:[0-9a-f]{64}$/.test(named)) record = await media.byFingerprint(named);
        if (!record && hex) {
          const transcript = await store.get(hex);
          if (transcript && transcript.kind === "file") record = await media.byFingerprint(transcript.media);
        }
        if (!record) {
          json(response, 404, { error: "nixamp.com does not know that file yet. `nixamp hash FILE` tells it." });
          return;
        }
        const transcriptId = record.facts.fingerprint ? transcriptIdOf(record.facts.fingerprint) : null;
        const languages = transcriptId ? await store.languages(transcriptId) : [];
        if (format === "srt" || format === "vtt" || format === "txt") {
          const language = languageCode(url.searchParams.get("language"));
          if (language === null) {
            json(response, 400, { error: "language is a two-letter code" });
            return;
          }
          const transcript = transcriptId ? await store.get(transcriptId, language) : null;
          if (!transcript) {
            json(response, 404, { error: language ? `not written down in ${language}` : "not written down yet" });
            return;
          }
          const text = format === "srt" ? toSrt(transcript.lines) : format === "vtt" ? toVtt(transcript.lines) : toText(transcript.lines);
          const name = (record.name.replace(/\.[^.]+$/, "") || record.id.slice(0, 12)).replace(/[^\w.-]+/g, "_").slice(0, 60);
          response.writeHead(200, {
            ...CORS,
            "content-type": `${format === "vtt" ? "text/vtt" : "text/plain"}; charset=utf-8`,
            "content-length": Buffer.byteLength(text),
            "cache-control": "public, max-age=300",
            "content-disposition": `inline; filename="${name}${transcript.language ? `.${transcript.language}` : ""}.${format}"`,
          });
          response.end(text);
          return;
        }
        const descriptor = openFileOf(record, site, languages);
        const accept = String(request.headers["accept"] ?? "");
        const wantsJson = viaApi || format === "json" || format === "openfile.json" || (accept.includes("application/json") && !accept.includes("text/html"));
        if (wantsJson) {
          json(response, 200, descriptor);
          return;
        }
        const original = transcriptId ? await store.get(transcriptId) : null;
        const forPage: PageTranscript[] = languages
          .slice()
          .sort((a, b) => (a.translatedFrom === null ? -1 : 0) - (b.translatedFrom === null ? -1 : 0))
          .map((one) => (original && one.language === original.language && one.translatedFrom === null ? { ...one, shown: original.lines } : one));
        html(response, 200, mediaPage(record, site, forPage));
      } catch (error) {
        if (error instanceof SpeechError) json(response, error.status, { error: error.message });
        else throw error;
      }
      return;
    }

    /*
     * Translation: texts in one language, said in another, by a model on
     * this CPU (see translate.ts). Signed in only, like the ear. GET says
     * which languages, and which each can be turned into here.
     */
    if (path === "/api/v1/translate" && options.accounts) {
      const translator = options.translator;
      if (request.method === "GET") {
        json(response, 200, {
          available: translator !== undefined,
          languages: Object.entries(LANGUAGES).map(([code, names]) => ({ code, ...names, targets: translator ? translator.targets(code) : [] })),
        });
        return;
      }
      if (request.method !== "POST") {
        json(response, 405, { error: "POST texts, or GET the languages" });
        return;
      }
      if (!translator) {
        json(response, 503, { error: "this nixamp cannot translate: the models are not here. nixamp.com can." });
        return;
      }
      const who = await options.accounts.whoIs(tokenFrom(request.headers));
      if (who === null) {
        json(response, 401, { error: "sign in to nixamp.com to translate" });
        return;
      }
      let body: { texts?: unknown; text?: unknown; from?: unknown; to?: unknown } = {};
      try {
        body = JSON.parse(await readBody(request, 256 * 1024)) as typeof body;
      } catch {
        json(response, 400, { error: "bad JSON" });
        return;
      }
      const texts = Array.isArray(body.texts)
        ? body.texts.filter((one): one is string => typeof one === "string")
        : typeof body.text === "string" ? [body.text] : [];
      if (texts.length === 0) {
        json(response, 400, { error: "say what: texts, a list of strings" });
        return;
      }
      if (texts.length > 200) {
        json(response, 413, { error: "200 texts at a time at most" });
        return;
      }
      const from = languageCode(body.from);
      const to = languageCode(body.to);
      if (from === null || to === null || to === "") {
        json(response, 400, { error: "from and to are two-letter language codes, such as en and sv" });
        return;
      }
      if (from === "") {
        json(response, 400, { error: "say which language the texts are in: from" });
        return;
      }
      try {
        json(response, 200, await translator.translate(texts, from, to, { by: who.id, ...(url.searchParams.get("live") === "1" ? { deadline: Date.now() + 10_000 } : {}) }));
      } catch (error) {
        if (error instanceof SpeechError) json(response, error.status, { error: error.message });
        else throw error;
      }
      return;
    }

    /*
     * The transcript store: what a file, a link or a live said, kept once
     * under the media's identity (see transcripts.ts). Signed in to read
     * and to keep, the way the ear is: a captioning server, the CLI, the
     * MCP tools. A language asks for a translation, made before answering
     * when it is short and as a job when it is not; a format asks for it
     * as subtitles.
     */
    if ((path === "/api/v1/transcripts" || path.startsWith("/api/v1/transcripts/")) && options.transcripts && options.translations && options.accounts) {
      const store = options.transcripts;
      const parts = path.slice("/api/v1/transcripts".length).split("/").filter(Boolean);
      const who = await options.accounts.whoIs(tokenFrom(request.headers));
      if (who === null) {
        json(response, 401, { error: "sign in to nixamp.com to read or keep transcripts" });
        return;
      }
      try {
        if (parts.length === 0) {
          if (request.method !== "GET") {
            json(response, 405, { error: "GET: what you have had written down" });
            return;
          }
          json(response, 200, { transcripts: await store.list(who.id, Number(url.searchParams.get("limit") ?? 50) || 50) });
          return;
        }
        let named = "";
        try {
          named = decodeURIComponent(parts[0] as string);
        } catch {
          json(response, 400, { error: "bad id" });
          return;
        }
        // By id, or by the media identity itself: both name the same row.
        const id = idFrom(named);
        const action = parts[1];
        if (action === undefined && request.method === "GET") {
          const language = languageCode(url.searchParams.get("language"));
          const format = formatOf(url.searchParams.get("format"));
          if (language === null) {
            json(response, 400, { error: "language is a two-letter code, or original" });
            return;
          }
          if (format === null) {
            json(response, 400, { error: "format is json, srt, vtt or txt" });
            return;
          }
          // Joining a live must not start translating an entire old transcript.
          if (url.searchParams.get("cached") === "1") {
            const saved = await store.get(id, language);
            json(response, saved ? 200 : 404, saved ? wire(saved) : { error: "no cached transcript" });
            return;
          }
          const answer = await options.translations.get(id, language, who.id);
          if (answer.status !== 200 && answer.status !== 202) {
            json(response, answer.status, { error: answer.error });
            return;
          }
          if (answer.status === 202) {
            json(response, 202, {
              ...(answer.transcript ? wire(answer.transcript, await store.languages(id)) : { id, language, lines: [], languages: await store.languages(id) }),
              translating: answer.translating,
            });
            return;
          }
          const transcript = answer.transcript;
          if (format === "json") {
            json(response, 200, wire(transcript, await store.languages(id)));
            return;
          }
          const text = format === "srt" ? toSrt(transcript.lines) : format === "vtt" ? toVtt(transcript.lines) : toText(transcript.lines);
          const name = (transcript.title || id).replace(/[^\w.-]+/g, "_").slice(0, 60) || "transcript";
          response.writeHead(200, {
            ...CORS,
            "content-type": `${format === "vtt" ? "text/vtt" : "text/plain"}; charset=utf-8`,
            "content-length": Buffer.byteLength(text),
            "cache-control": "no-store",
            "content-disposition": `inline; filename="${name}${transcript.language ? `.${transcript.language}` : ""}.${format}"`,
          });
          response.end(text);
          return;
        }
        if (action === "lines" && request.method === "POST") {
          let body: { media?: unknown; language?: unknown; translatedFrom?: unknown; model?: unknown; title?: unknown; complete?: unknown; lines?: unknown } = {};
          try {
            body = JSON.parse(await readBody(request, 4 * 1024 * 1024)) as typeof body;
          } catch {
            json(response, 400, { error: "bad JSON, or more than 4 MB of it" });
            return;
          }
          const media = typeof body.media === "string" ? body.media.trim() : "";
          if (media === "" || media.length > 2048 || !/^(file|url|live):/.test(media)) {
            json(response, 400, { error: "media is the identity: file:v1:<hash>, url:<address> or live:<server>/<channel>@<started>" });
            return;
          }
          if (transcriptIdOf(media) !== id) {
            json(response, 400, { error: "that media is not this transcript's id" });
            return;
          }
          const language = languageCode(body.language);
          const translatedFrom = body.translatedFrom === undefined || body.translatedFrom === null ? "" : languageCode(body.translatedFrom);
          if (language === null || translatedFrom === null) {
            json(response, 400, { error: "language and translatedFrom are two-letter codes" });
            return;
          }
          const lines = linesFrom(body.lines);
          if (lines.length === 0 && body.complete !== true) {
            json(response, 400, { error: "lines: a list of {start, end, text}, seconds into the media" });
            return;
          }
          const saved = await store.save({
            media,
            language,
            translatedFrom: translatedFrom === "" ? null : translatedFrom,
            model: typeof body.model === "string" ? body.model.slice(0, 100) : "",
            title: typeof body.title === "string" ? body.title : "",
            complete: body.complete === true,
            by: who.id,
            lines,
          });
          json(response, 200, { id, language: saved?.language ?? language, saved: lines.length, seconds: saved?.seconds ?? 0, complete: saved?.complete ?? false });
          return;
        }
        if (action === undefined && request.method === "DELETE") {
          const gone = await store.forget(id, who.id);
          json(response, gone ? 200 : 404, gone ? { ok: true } : { error: "nothing of yours under that id" });
          return;
        }
        json(response, 405, { error: "GET a transcript, POST its lines, or DELETE it" });
      } catch (error) {
        if (error instanceof SpeechError) json(response, error.status, { error: error.message });
        else throw error;
      }
      return;
    }

    /*
     * MCP over HTTP: the tools `nixamp mcp` offers on a pipe, at
     * nixamp.com/mcp for an agent with no nixamp installed. A JSON-RPC
     * message, or a batch of them, in a POST; the answers as JSON. The
     * bearer token says whose tools they are, as on every other route, and
     * the tools call nixamp.com as that account. No stream is offered on
     * GET: nothing here has to be pushed, so a client is told so and gets
     * on with POST.
     */
    /*
     * nixamp.com/mcp/public: what is on, anywhere, for any agent with no
     * account. /mcp stays a 401 without a token, because that 401 is how an
     * MCP client finds out it should sign in (OAuth discovery).
     */
    if (path === "/mcp/public" && options.stations) {
      if (request.method === "OPTIONS") { response.writeHead(204, CORS); response.end(); return; }
      if (request.method !== "POST") {
        json(response, 405, { error: "POST a JSON-RPC message" });
        return;
      }
      const stationGuide = async (): Promise<unknown[]> => {
        if (!options.stations) return [];
        const own = guideEntries({
          store: options.stations.store, runner: options.stations.runner, channels: options.channels!, canControl: false, account: "",
          select: () => null, trackFor: (file) => trackFor(engine, file), base: options.site ?? "", publicBase: options.stations.publicBase?.() ?? "",
          json, readBody, headers: CORS,
        });
        const shown = new Set(own.map((one) => one.page));
        const others = options.stations.peers ? (await options.stations.peers.entries()).filter((one) => !shown.has(one.page)) : [];
        return [...own, ...others];
      };
      let parsed: unknown;
      try {
        parsed = JSON.parse(await readBody(request, 256 * 1024));
      } catch {
        json(response, 400, { jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } });
        return;
      }
      const messages = (Array.isArray(parsed) ? parsed : [parsed]) as Array<{ id?: unknown; method?: unknown; params?: Record<string, unknown> }>;
      const answers: unknown[] = [];
      for (const message of messages) {
        const id = message?.id ?? null;
        const method = typeof message?.method === "string" ? message.method : "";
        if (method.startsWith("notifications/")) continue;
        if (method === "initialize") {
          answers.push({ jsonrpc: "2.0", id, result: {
            protocolVersion: (message.params?.["protocolVersion"] as string | undefined) ?? "2025-06-18",
            capabilities: { tools: { listChanged: false } },
            serverInfo: { name: "nixamp", title: "nixamp: every public radio and TV station", version: "2" },
            instructions: "Find what is on across every nixamp server (stations_list, station_find) and share a track to a station's server (station_share). Sign in (a nixamp token as the bearer, or OAuth) for watch parties, rooms, transcripts and managing your own stations.",
          } });
        } else if (method === "ping") {
          answers.push({ jsonrpc: "2.0", id, result: {} });
        } else if (method === "tools/list") {
          answers.push({ jsonrpc: "2.0", id, result: { tools: PUBLIC_TOOLS } });
        } else if (method === "tools/call" && isPublicTool(String(message.params?.["name"] ?? ""))) {
          answers.push({ jsonrpc: "2.0", id, result: await callPublicTool(String(message.params!["name"]), (message.params?.["arguments"] ?? {}) as Record<string, unknown>, { guide: stationGuide }) });
        } else {
          answers.push({ jsonrpc: "2.0", id, error: { code: -32001, message: "only the station tools are public; for the rest use /mcp with a nixamp token as the bearer (`nixamp token create`), or OAuth" } });
        }
      }
      if (answers.length === 0) {
        response.writeHead(202, CORS);
        response.end();
        return;
      }
      json(response, 200, Array.isArray(parsed) ? answers : answers[0]);
      return;
    }

    if (path === "/mcp" && options.accounts && options.site) {
      if (request.method === "GET") {
        response.writeHead(405, { ...CORS, allow: "POST, DELETE" });
        response.end();
        return;
      }
      if (request.method === "DELETE") {
        response.writeHead(204, CORS);
        response.end();
        return;
      }
      if (request.method !== "POST") {
        json(response, 405, { error: "POST a JSON-RPC message" });
        return;
      }
      const token = tokenFrom(request.headers);
      const who = await options.accounts.whoIs(token);
      const stationGuide = async (): Promise<unknown[]> => {
        if (!options.stations) return [];
        const own = guideEntries({
          store: options.stations.store, runner: options.stations.runner, channels: options.channels!, canControl: false, account: "",
          select: () => null, trackFor: (file) => trackFor(engine, file), base: options.site ?? "", publicBase: options.stations.publicBase?.() ?? "",
          json, readBody, headers: CORS,
        });
        const shown = new Set(own.map((one) => one.page));
        const others = options.stations.peers ? (await options.stations.peers.entries()).filter((one) => !shown.has(one.page)) : [];
        return [...own, ...others];
      };
      if (who === null) {
        const error = JSON.stringify({ error: "sign in: a nixamp token as the bearer (`nixamp token create`), or OAuth" });
        response.writeHead(401, {
          ...CORS,
          "content-type": "application/json; charset=utf-8",
          "content-length": Buffer.byteLength(error),
          "www-authenticate": `Bearer resource_metadata="${options.site}/.well-known/oauth-protected-resource"`,
        });
        response.end(error);
        return;
      }
      let parsed: unknown;
      try {
        parsed = JSON.parse(await readBody(request, 1024 * 1024));
      } catch {
        json(response, 400, { jsonrpc: "2.0", id: null, error: { code: -32700, message: "parse error" } });
        return;
      }
      const messages = Array.isArray(parsed) ? parsed : [parsed];
      const session = { site: options.site, token };
      const answers: unknown[] = [];
      for (const message of messages) {
        if (!message || typeof message !== "object" || typeof (message as { method?: unknown }).method !== "string") {
          answers.push({ jsonrpc: "2.0", id: null, error: { code: -32600, message: "invalid request" } });
          continue;
        }
        const answer = await mcpMessage(message as Parameters<typeof mcpMessage>[0], { session, fetcher: fetch, guide: stationGuide });
        if (answer) answers.push(answer);
      }
      if (answers.length === 0) {
        response.writeHead(202, CORS);
        response.end();
        return;
      }
      json(response, 200, Array.isArray(parsed) ? answers : answers[0]);
      return;
    }

    /* Where a client of /mcp finds the authorization server (RFC 9728). */
    if (path === "/.well-known/oauth-protected-resource" && options.site && options.authServer) {
      json(response, 200, {
        resource: options.site,
        authorization_servers: [options.site],
        bearer_methods_supported: ["header"],
        scopes_supported: SCOPE_NAMES,
        resource_name: "nixamp",
      });
      return;
    }

    // A listen pass (listen-pass.ts): a station page on any *.nixamp.com server
    // asks for one with this site's cookie, which is same-site there, so a
    // signed-in listener hears no breaks. Credentialed, so never "*".
    if (path === "/api/v1/me/pass" && request.method === "GET" && options.accounts && listenPassKey) {
      const origin = String(request.headers.origin ?? "");
      const cors: Record<string, string> = /^https:\/\/([a-z0-9-]+\.)*nixamp\.com(:\d+)?$/.test(origin)
        ? { "access-control-allow-origin": origin, "access-control-allow-credentials": "true", vary: "origin" }
        : {};
      const who = await options.accounts.whoIs(tokenFrom(request.headers));
      const body = who === null ? { error: "not signed in" } : mintListenPass(listenPassKey);
      response.writeHead(who === null ? 401 : 200, { ...cors, "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
      response.end(JSON.stringify(body));
      return;
    }
    // Asked by a station on another server about a pass it was handed.
    if (path === "/api/v1/listen-pass/check" && request.method === "GET" && listenPassKey) {
      const expires = checkListenPass(listenPassKey, url.searchParams.get(PASS_PARAM) ?? "");
      json(response, 200, expires > 0 ? { ok: true, expires } : { ok: false });
      return;
    }

    if (path === "/api/v1/me/handle" && options.handles && options.accounts) {
      const handles = options.handles;
      const who = await options.accounts.whoIs(tokenFrom(request.headers));
      if (who === null) {
        json(response, 401, { error: "not signed in" });
        return;
      }

      // The handle, and since 0.23.4 the voice a line is read in on the
      // phone and the OpenProfile it may be read from. Any of the three may
      // be sent alone; what is not sent is kept.
      if (request.method === "GET") {
        const persona = await handles.persona(who.id);
        // And the voice a line of theirs would be read in right now, so the
        // panel and the CLI can say it rather than describe the rule.
        const pools = options.voices ? await options.voices.pools() : null;
        const profile = persona.profile && options.profiles ? await options.profiles.voiceOf(persona.profile) : null;
        const spoken = pools ? spokenVoiceFor({ userId: who.id, voice: persona.voice, profile }, pools).voice : "";
        json(response, 200, { ...persona, handle: persona.handle || fallbackHandle(who.id), chosen: persona.handle !== "", spoken });
        return;
      }
      if (request.method === "PUT" || request.method === "POST") {
        let body: { handle?: unknown; voice?: unknown; profile?: unknown };
        try {
          body = JSON.parse(await readBody(request)) as typeof body;
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        if (body.handle !== undefined) {
          const claimed = await handles.claim(who.id, body.handle);
          if (claimed.error) {
            json(response, 409, { error: claimed.error });
            return;
          }
        }
        if (body.voice !== undefined || body.profile !== undefined) {
          const described = await handles.describe(who.id, fallbackHandle(who.id), { voice: body.voice, profile: body.profile });
          if (described.error) {
            json(response, 422, { error: described.error });
            return;
          }
        }
        const persona = await handles.persona(who.id);
        json(response, 200, { ...persona, handle: persona.handle || fallbackHandle(who.id), chosen: persona.handle !== "" });
        return;
      }
      json(response, 405, { error: "GET or PUT" });
      return;
    }

    // --- the account's card -------------------------------------------------
    //
    // A name, a homepage, a photo and a line, said once. A class is made from
    // it, a settings page edits it, and an OpenProfile.md fills it in one move.
    if ((path === "/api/v1/me/profile" || path.startsWith("/api/v1/me/profile/")) && options.accountProfiles && options.accounts) {
      const profiles = options.accountProfiles;
      const who = await options.accounts.whoIs(tokenFrom(request.headers));
      if (who === null) {
        json(response, 401, { error: "not signed in" });
        return;
      }
      const persona = options.handles ? await options.handles.persona(who.id) : null;
      const answer = async (): Promise<void> => {
        const profile = await profiles.of(who.id);
        // Read again: an import may just have remembered the address.
        const now = options.handles ? await options.handles.persona(who.id) : null;
        json(response, 200, {
          ...profile,
          card: hostCard(profile, who.id, accountSite),
          openProfile: now?.profile ?? "",
          handle: now?.handle || fallbackHandle(who.id),
        });
      };

      if (path === "/api/v1/me/profile") {
        if (request.method === "GET") { await answer(); return; }
        if (request.method === "PUT" || request.method === "POST") {
          let body: Record<string, unknown>;
          try {
            body = JSON.parse(await readBody(request)) as Record<string, unknown>;
          } catch {
            json(response, 400, { error: "bad JSON" });
            return;
          }
          const written = await profiles.set(who.id, body);
          if (written.error) { json(response, 422, { error: written.error }); return; }
          await answer();
          return;
        }
        json(response, 405, { error: "GET or PUT" });
        return;
      }

      if (path === "/api/v1/me/profile/import") {
        if (request.method !== "POST") { json(response, 405, { error: "POST only" }); return; }
        let body: { url?: unknown } = {};
        try {
          const raw = await readBody(request);
          body = raw.trim() === "" ? {} : (JSON.parse(raw) as typeof body);
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        const given = typeof body.url === "string" ? body.url.trim() : "";
        const from = given || persona?.profile || "";
        if (from === "") {
          json(response, 422, { error: "give the URL of your OpenProfile.md, like https://you.example/.well-known/openprofile.md" });
          return;
        }
        const imported = await profiles.importFrom(who.id, from);
        if (imported.error) { json(response, 422, { error: imported.error }); return; }
        // The address is remembered, so the phone line reads the same file
        // for the voice, and a later import needs no URL.
        if (given && options.handles && given !== persona?.profile) {
          await options.handles.describe(who.id, fallbackHandle(who.id), { profile: given });
        }
        await answer();
        return;
      }

      if (path === "/api/v1/me/profile/photo") {
        if (request.method === "PUT" || request.method === "POST") {
          let bytes: Uint8Array;
          try {
            bytes = await readBytes(request, PHOTO_LIMIT);
          } catch {
            json(response, 413, { error: "a photo may be up to 1 MB" });
            return;
          }
          const kept = await profiles.setPhoto(who.id, bytes);
          if (kept.error) { json(response, 422, { error: kept.error }); return; }
          await answer();
          return;
        }
        if (request.method === "DELETE") {
          await profiles.removePhoto(who.id);
          await answer();
          return;
        }
        json(response, 405, { error: "PUT or DELETE" });
        return;
      }
      json(response, 404, { error: "no such profile route" });
      return;
    }

    // The stored photo, public: it is on every class the host runs, and a
    // page shows it to strangers by design. Cached by its etag.
    if (path.startsWith("/api/v1/profiles/") && path.endsWith("/photo") && options.accountProfiles) {
      if (request.method !== "GET" && request.method !== "HEAD") { json(response, 405, { error: "GET only" }); return; }
      const userId = decodeURIComponent(path.slice("/api/v1/profiles/".length, -"/photo".length));
      const photo = userId === "" ? null : await options.accountProfiles.photo(userId);
      if (photo === null) { json(response, 404, { error: "no photo" }); return; }
      const etag = `"${photo.etag}"`;
      if (request.headers["if-none-match"] === etag) {
        response.writeHead(304, { etag, "cache-control": "public, max-age=86400" });
        response.end();
        return;
      }
      response.writeHead(200, {
        "content-type": photo.type,
        "content-length": photo.bytes.length,
        etag,
        "cache-control": "public, max-age=86400",
        "x-content-type-options": "nosniff",
        "access-control-allow-origin": "*",
      });
      response.end(request.method === "HEAD" ? undefined : Buffer.from(photo.bytes));
      return;
    }

    // --- the servers this account runs ----------------------------------
    //
    // Kept against the account rather than the machine, so the list reads the
    // same from the CLI, the PWA and the desktop app -- which is the whole
    // point: a share link in a terminal you closed is a server you have lost.
    if ((path === "/api/v1/servers" || path.startsWith("/api/v1/servers/")) && options.servers && options.accounts) {
      const servers = options.servers;
      const token = tokenFrom(request.headers);
      const who = await options.accounts.whoIs(token);
      if (who === null) {
        json(response, 401, { error: "not signed in" });
        return;
      }
      // An app acting for the person sees the list only with a scope that
      // says so, may not change it, and never gets a control key: those are
      // the person's, and an app that held one could drive the machine.
      const grant = options.authServer ? await options.authServer.grantFor(token) : null;
      if (grant) {
        if (!(grant.scope.includes("streams") || grant.scope.includes("stations"))) {
          json(response, 403, { error: "this app was not given access to your servers", code: "insufficient_scope" });
          return;
        }
        if (!(path === "/api/v1/servers" && request.method === "GET")) {
          json(response, 403, { error: "an app may list your servers, not change them", code: "insufficient_scope" });
          return;
        }
        json(response, 200, { servers: (await servers.list(who.id)).map(({ key: _key, ...shown }) => shown) });
        return;
      }

      if (path === "/api/v1/servers" && request.method === "GET") {
        json(response, 200, { servers: await servers.list(who.id) });
        return;
      }

      if (path === "/api/v1/servers" && request.method === "POST") {
        let body: { name?: unknown; url?: unknown; key?: unknown };
        try {
          body = JSON.parse(await readBody(request)) as typeof body;
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        const made = await servers.add(who, {
          ...(typeof body.name === "string" ? { name: body.name } : {}),
          ...(typeof body.url === "string" ? { url: body.url } : {}),
          ...(typeof body.key === "string" ? { key: body.key } : {}),
        });
        if (made === null) {
          json(response, 422, { error: "that needs an http or https address" });
          return;
        }
        json(response, 201, { server: made });
        return;
      }

      const id = path.slice("/api/v1/servers/".length);
      if (id && request.method === "DELETE") {
        const gone = await servers.remove(who.id, id);
        json(response, gone ? 200 : 404, gone ? { ok: true } : { error: "no such server" });
        return;
      }

      if (id && (request.method === "PATCH" || request.method === "PUT")) {
        let body: { name?: unknown; url?: unknown; key?: unknown };
        try {
          body = JSON.parse(await readBody(request)) as typeof body;
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        const changed = await servers.update(who.id, id, {
          ...(typeof body.name === "string" ? { name: body.name } : {}),
          ...(typeof body.url === "string" ? { url: body.url } : {}),
          ...(typeof body.key === "string" ? { key: body.key } : {}),
        });
        if (changed === null) {
          json(response, 404, { error: "no such server, or a bad address" });
          return;
        }
        json(response, 200, { server: changed });
        return;
      }

      json(response, 405, { error: "GET, POST, PATCH or DELETE" });
      return;
    }

    // --- open directories somebody found -----------------------------------
    //
    // Its own list, not the stream directory: that one is what is playing now,
    // with a heartbeat and a room code and somebody at the other end, and this
    // one is a folder on the web that is always there and belongs to nobody
    // here. Reading needs no account. Adding does, because a public list with
    // nobody accountable for its rows is a list of whatever anyone felt like.
    if ((path === "/api/v1/opendirs" || path.startsWith("/api/v1/opendirs/")) && options.openDirs) {
      const dirs = options.openDirs;

      if (path === "/api/v1/opendirs" && request.method === "GET") {
        const limit = Number(url.searchParams.get("limit") ?? "50");
        const before = Number(url.searchParams.get("before") ?? "0");
        const page = await dirs.list(Number.isFinite(limit) ? limit : 50, Number.isFinite(before) ? before : 0);
        const names = options.handles ? await options.handles.many(page.addedBy) : new Map<string, string>();
        json(response, 200, {
          opendirs: page.rows.map((row, at) => ({
            ...row,
            // A handle or nothing. The address that signed up is never here.
            by: names.get(page.addedBy[at] ?? "") ?? "",
          })),
          next: page.next,
        });
        return;
      }

      if (path === "/api/v1/opendirs" && request.method === "POST" && options.accounts) {
        const who = await options.accounts.whoIs(tokenFrom(request.headers));
        if (who === null) {
          json(response, 401, { error: "sign in to publish one" });
          return;
        }
        let body: { url?: unknown; name?: unknown };
        try {
          body = JSON.parse(await readBody(request)) as typeof body;
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }

        // Read before publishing. A row nobody can play is worse than no row,
        // and the count is the one fact a reader wants before clicking.
        const listed = typeof body.url === "string" ? await readRemoteIndex(body.url) : [];
        if (listed.length === 0) {
          json(response, 422, { error: "nothing playable was linked from that page" });
          return;
        }
        const made = await dirs.add(who.id, body.url, body.name, listed.length);
        if (made === null) {
          json(response, 422, { error: "that needs an http or https address" });
          return;
        }
        const handle = options.handles ? await options.handles.of(who.id) : "";
        json(response, 201, { opendir: { ...made, by: handle } });
        return;
      }

      const dirId = path.slice("/api/v1/opendirs/".length);
      if (dirId && request.method === "DELETE" && options.accounts) {
        const who = await options.accounts.whoIs(tokenFrom(request.headers));
        if (who === null) {
          json(response, 401, { error: "not signed in" });
          return;
        }
        const gone = await dirs.remove(who.id, dirId);
        json(response, gone ? 200 : 404, gone ? { ok: true } : { error: "not yours, or not there" });
        return;
      }

      json(response, 405, { error: "GET, POST or DELETE" });
      return;
    }

    // --- coming back from a provider ---------------------------------------
    //
    // /api/v1/<provider>/oauth/start sends a browser away, and .../callback is
    // what the provider was told to send it back to. Both are outside the
    // /api/v1/auth/ block because that is the URL shape registered with GitHub
    // and Google, and a redirect URI is not something to change lightly.
    const oauthRoute = OAUTH_ROUTE.exec(path);
    if (oauthRoute && options.accounts && options.signIn) {
      const accounts = options.accounts;
      const signIn = options.signIn;
      const secure = options.secureCookies ?? false;
      const provider = signIn.provider(oauthRoute[1]);
      if (provider === null) {
        json(response, 404, { error: "this nixamp cannot sign you in with that" });
        return;
      }

      if (oauthRoute[2] === "start") {
        // A terminal can link straight here with the code it is showing, which
        // is one hop shorter than the page for somebody who followed the link.
        const grant = signIn.device.find(url.searchParams.get("device") ?? "");
        // Back where the visit was going: a page here, or a site that sends people here to sign in.
        const redirect = safeRedirect(url.searchParams.get("redirect"), signInReturnHosts());
        response.writeHead(302, { location: signIn.begin(provider, grant?.userCode ?? "", redirect) });
        response.end();
        return;
      }

      // A callback carrying no state, or one whose state was already spent, is
      // not a sign-in: it is somebody replaying a URL they found.
      const pending = signIn.claim(url.searchParams.get("state"));
      if (pending === null || pending.provider !== provider.id) {
        html(response, 400, signInFailedPage("That sign-in link has expired. Start again."));
        return;
      }
      const code = url.searchParams.get("code") ?? "";
      if (!code) {
        html(response, 400, signInFailedPage(url.searchParams.get("error") ?? "The provider sent no code."));
        return;
      }

      const access = await exchangeCode(provider, code, signIn.site, fetch, pending.verifier ?? "").catch(() => "");
      const identity = access ? await provider.identify(access, fetch).catch(() => null) : null;
      if (identity === null) {
        html(response, 401, signInFailedPage(provider.pkce ? `${provider.name} did not finish the sign-in. Try it again.` : `${provider.name} did not confirm a verified email address.`));
        return;
      }

      const result = await accounts.signInWith(identity);
      if (!result.ok || result.account === null) {
        html(response, 401, signInFailedPage(result.error || "Could not sign in."));
        return;
      }

      if (pending.userCode) {
        // This round trip was approving a terminal. The browser is finished;
        // the session belongs to whatever is polling.
        if (!signIn.device.approve(pending.userCode, { token: result.token, email: result.account.email })) {
          html(response, 410, signInFailedPage("That terminal stopped waiting. Run `nixamp login` again."));
          return;
        }
        html(response, 200, deviceDonePage(result.account.email));
        return;
      }

      response.writeHead(302, {
        "set-cookie": sessionCookie(result.token, secure),
        location: pending.redirect || "/",
      });
      response.end();
      return;
    }

    // The directory is public to read and answered before the key check,
    // because a visitor to nixamp.com has no key and is exactly who it is for.
    //
    // Announcing is not public any more. A listing now carries a phone number
    // people dial and minutes we pay for, so it has to be attributable to
    // somebody: broadcasters register, and a caller just dials. Reading stays
    // open to everyone -- the whole point is a directory a stranger can browse.
    if (path === "/api/directory" && options.directory) {
      if (request.method === "GET") {
        // Each stream carries its call-in code and how many people are on the
        // phone for it. The code is published on purpose: it is a public
        // call-in line, and a listing you cannot dial is a listing of nothing.
        const onThePhone = options.partyLine;
        // The admin link goes only to the account that owns the listing. A
        // directory that handed out control links would be a directory of
        // machines anyone could take over.
        const me = options.accounts ? await options.accounts.whoIs(tokenFrom(request.headers)) : null;
        const streams = options.directory.list().map(({ admin, ...stream }) => ({
          ...stream,
          callers: onThePhone ? onThePhone.listenersOn(stream.code) : 0,
          // And on the phone for each live on it, by name: every live is its
          // own room, so each has its own count.
          channelCallers: Object.fromEntries(
            Object.entries(stream.channelCodes).map(([name, code]) => [name, onThePhone ? onThePhone.listenersOn(code) : 0]),
          ),
          ...(admin && me !== null && stream.ownerId === me.id ? { admin } : {}),
        }));
        // Recently ended too, because following exists to hear about
        // broadcasts you would otherwise miss -- and a list of only what is on
        // can only be used to follow somebody during a broadcast you did not
        // miss. No url and no code: there is nothing to listen to.
        const recent = options.directory.recentlyEnded().map((stream) => ({
          name: stream.name,
          ownerId: stream.ownerId,
          nowPlaying: stream.nowPlaying,
          endedAt: stream.endedAt,
        }));
        json(response, 200, { streams, recent, callIn: CALL_IN_NUMBER, now: Date.now() });
        return;
      }
      if (request.method === "POST") {
        // Only where there are accounts to check against. An instance with no
        // Accounts is somebody's laptop, which has no registration to demand.
        let ownerId = "";
        if (options.accounts) {
          const who = await options.accounts.whoIs(tokenFrom(request.headers));
          if (who === null) {
            json(response, 401, {
              error: "sign in to list a stream: nixamp login, then nixamp serve --directory",
            });
            return;
          }
          // From the token, never the body. A stream that could name its own
          // owner could name somebody else's, and their followers would be
          // told about a broadcast that person is not making.
          ownerId = who.id;
        }

        let announcement;
        try {
          announcement = parseAnnouncement(JSON.parse(await readBody(request)));
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        if (announcement === null) {
          json(response, 422, { error: "a listing needs a name and a URL a browser can reach" });
          return;
        }
        const listing = options.directory.announce(announcement, ownerId);
        // A stream reappearing is the event somebody asked to be told about.
        // Answered first and texted after, because the publisher's heartbeat
        // should not wait on an SMS gateway.
        json(response, 200, listing);
        if (options.partyLine) {
          void options.partyLine
            .wentLive({ code: listing.code, name: listing.name, nowPlaying: listing.nowPlaying })
            .catch(() => {});
        }
        return;
      }
      if (request.method === "DELETE") {
        const id = url.searchParams.get("id") ?? "";
        const listing = options.directory.list().find((one) => one.id === id);
        if (!listing) {
          // Already gone, or never here. Saying so plainly rather than
          // pretending to have done something.
          json(response, 404, { error: "no such listing" });
          return;
        }

        // Taking a stream out of a public directory is the owner's to do.
        //
        // This asked nobody anything: a listing id is in every copy of the
        // list, so anyone who could read the directory could empty it of other
        // people's streams. The publisher already sends its token; it was
        // simply never looked at.
        const who = options.accounts ? await options.accounts.whoIs(tokenFrom(request.headers)) : null;
        const mine = listing.ownerId !== "" && who !== null && who.id === listing.ownerId;
        if (!mine) {
          json(response, 403, {
            error: listing.ownerId === ""
              ? "that listing has no owner to prove; it leaves the list when it stops renewing"
              : "only the account that published a stream can take it off the list",
          });
          return;
        }

        options.directory.withdraw(id);
        json(response, 200, { ok: true });
        return;
      }
      json(response, 405, { error: "GET, POST or DELETE" });
      return;
    }

    // Administering is a different question from listening, and it is asked
    // after the share key: the control key answers both, but a listen key or a
    // nixamp.com session answers only one of them.
    /**
     * The member this request is from, when it is a member going live rather
     * than the owner administering: the nixamp.com account id. Read by the
     * handlers that put things on the air, to mark what they put on and to
     * count it against what a member may have on at once.
     */
    let liveBy = "";
    const eventMediaOperation = Boolean(
      channelId &&
      ((request.method === "POST" && (channelAction === undefined || channelAction === "chunk")) ||
        (request.method === "DELETE" && channelAction === undefined)) &&
      eventChannelRequest,
    );
    if (options.owner && needsAdmin(path, request.method ?? "GET") && !eventMediaOperation) {
      // A server started with --no-key has said that anyone who can reach the
      // port may drive it, and prints exactly that. Locking administration to
      // nobody would contradict it and leave such a server unadministrable.
      const holdsControl =
        key === null || scopeOf(keyFrom(request, url), key, null) === "control";
      // A session in the query as well as in a header or a cookie: from
      // nixamp.com's page a server is another origin, and the cookie stays
      // home, so the session travels the way the key does.
      const sessionToken = tokenFrom(request.headers) || url.searchParams.get("session") || "";
      const check = await options.owner.check(holdsControl, sessionToken);
      // Not the owner, but somebody: a signed-in nixamp.com account is a
      // member, and a member may go live here.
      const member = check.allowed ? "" : await options.owner.accountFor(sessionToken);

      if (path === "/api/admin") {
        // Always answered, and honestly: the page has to know whether to draw
        // an admin panel at all, and "no" is a real answer rather than a 403.
        json(response, 200, { allowed: check.allowed, as: check.as, claimed: options.owner.claimed, member: member !== "" });
        return;
      }
      if (!check.allowed) {
        const ownedChannel = channelId === "" ? "" : options.channels?.info(channelId)?.startedBy ?? "";
        const memberOwnsChannel = member !== "" && ownedChannel === member;
        if (member !== "" && (needsMember(path, request.method ?? "GET") || memberOwnsChannel)) {
          // Metered per member, before anything is done: going live is a
          // decoder on this machine, and a script could ask for one a
          // second. Three a minute is a person; more is not.
          if (needsMember(path, request.method ?? "GET")) {
            const refused = await memberThrottle.handle(requestFor(request));
            if (refused) {
              await answerWith(response, refused);
              return;
            }
          }
          liveBy = member;
        } else {
          json(response, 403, {
            error: options.owner.claimed
              ? "sign in to nixamp.com as this server's owner, or use its control link"
              : "this server has no owner signed in; use its control link",
          });
          return;
        }
      }
    }

    // After the key check: a paying listener still needs the link, and a 402
    // is a worse answer than a 401 to someone who has neither.
    if (options.paywall && (await options.paywall(request, response, path))) return;

    /**
     * What is live on this server, for somebody deciding what to watch.
     *
     * Two different things are, and they were only ever visible in two
     * different places: the server's own stream -- its playlist, which is what
     * it is listed in the directory as -- and any channels being published
     * into it from OBS or a phone. A person connected to a server had no way
     * to see either as a list, so "what is on here?" had no answer.
     *
     * Readable by anyone holding either link, because this is the viewing
     * side: it names streams and how to reach them, and says nothing about
     * how to change them.
     */
    if (path === "/api/streams") {
      const now = engine.snapshot(false);
      const state = options.live?.status();
      json(response, 200, {
        server: {
          name: options.serverName ?? "this server",
          nowPlaying: engine.snapshot().tracks?.[now.index]?.title ?? "",
          tracks: now.trackCount,
          playing: now.playing,
          /**
           * Live means playing, and it did not.
           *
           * It meant "listed in a directory", which is a different fact
           * entirely -- so a stopped server advertised a live stream of a film
           * nobody was watching, and everybody who joined started it from the
           * beginning on their own. There is nothing to be in sync with until
           * something is actually running.
           */
          live: state?.live === true && now.playing,
          /** Findable in the directory, which is what `live` used to mean. */
          listed: state?.live === true,
          // Only when it has been published: a code is a thing you dial, and
          // one nobody can dial is not worth showing.
          code: state?.code ?? "",
          url: state?.url ?? "",
          // Whether a link can go live here at all. The hosted directory
          // has no ffmpeg, and a page that offered to go live on it was
          // offering something that always failed.
          carries: options.carries !== false && Boolean(options.channels),
        },
        channels: (options.channels?.list() ?? []).filter((one) => !stationOfChannel(one.id)).map((one) => ({
          id: one.id,
          name: one.name,
          via: one.via,
          listeners: one.listeners,
          startedAt: one.startedAt,
          // Whether it has a picture, so the page puts it in the element
          // that can show one. Never the source: that is the owner's.
          kind: one.kind ?? "audio",
          // Its own room on the phone line, as the directory assigned it;
          // empty until the next heartbeat has told the directory it is on.
          code: state?.channelCodes?.[one.name] ?? "",
          // How it has been going, for whoever may do something about it.
          redials: one.redials ?? 0,
          error: one.error ?? "",
          // For a list: how long it is, and which entry is on, from 0.
          ...(one.playlist ? {
            entries: one.playlist.length,
            entry: one.playlistAt ?? 0,
            playlist: one.playlist.map((source) => {
              const root = engine.snapshot(false).root;
              if (root && !isRemote(source)) {
                const served = relative(root, source).replaceAll("\\", "/");
                if (served && !served.startsWith("../")) return served;
              }
              const parts = source.split(/[\\/]/).filter(Boolean);
              return parts.length > 1 ? parts.slice(-2).join(" / ") : (parts[0] || "Untitled");
            }),
            live: one.live !== false,
          } : {}),
          // The member who put it on, when one did: theirs to take off.
          startedBy: one.startedBy ?? "",
          // A picture of it, and a line about it, where there is one.
          art: artOf(one),
          about: one.about ?? "",
          // Over, with the outro playing: the page says so rather than "LIVE".
          ...(one.ended ? { ended: one.ended } : {}),
        })),
        // Anything re-streamed into this server is a live stream too, and was
        // sitting in the middle of the playlist among the files -- which is
        // what made moving between a channel and an album so confusing. Named
        // here with the first track it owns, so it can be played from the list
        // of what is live rather than hunted for among five thousand files.
        // Every stream coming off the network, however it got here.
        //
        // This used to read the groups, which only exist for sources that were
        // added -- so re-streaming something as a replacement produced a
        // channel that appeared in no list anywhere, with nothing to click.
        // What makes a thing live is where it comes from, not which button
        // loaded it.
        restreams: liveOnes(engine),
      });
      return;
    }

    // --- several streams at once ------------------------------------------
    //
    // --- catalogs: m3u lists you can browse ---------------------------------
    //
    // An IPTV list is thousands of entries with groups and logos. Kept as a
    // catalog it stays browsable; poured into the playlist it was three
    // thousand flat rows. Anyone with the link browses and plays; adding,
    // refreshing and removing is administering (see needsAdmin).
    if ((path === "/api/catalogs" || path.startsWith("/api/catalogs/")) && options.catalogs) {
      const catalogs = options.catalogs;

      if (path === "/api/catalogs" && request.method === "GET") {
        // Where a list is read from is the administrator's business, not a
        // listener's: it can carry a provider's credentials in the URL.
        const holdsControl = key === null || scopeOf(keyFrom(request, url), key, null) === "control";
        const admin = options.owner
          ? (await options.owner.check(holdsControl, tokenFrom(request.headers))).allowed
          : holdsControl;
        json(response, 200, { catalogs: catalogs.list().map((one) => shownCatalog(one, admin)) });
        return;
      }

      if (path === "/api/catalogs" && request.method === "POST") {
        let body: { source?: unknown; name?: unknown } = {};
        try {
          body = JSON.parse(await readBody(request)) as typeof body;
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        try {
          const added = await catalogs.add(String(body.source ?? ""), String(body.name ?? ""));
          json(response, added.error ? 422 : 200, {
            ok: !added.error,
            catalog: shownCatalog(added, true),
            ...(added.error ? { error: added.error } : {}),
          });
        } catch (error) {
          json(response, 422, { error: (error as Error).message.replace(/^nixamp: /, "") });
        }
        return;
      }

      const [rawId = "", action = "", entryId = "", sub = ""] = path.slice("/api/catalogs/".length).split("/");
      const id = decodeURIComponent(rawId);
      if (!catalogs.get(id)) {
        json(response, 404, { error: "no such catalog" });
        return;
      }

      if (action === "" && request.method === "DELETE") {
        json(response, 200, { ok: catalogs.remove(id) });
        return;
      }
      if (action === "refresh" && request.method === "POST") {
        const refreshed = await catalogs.refresh(id);
        json(response, refreshed && !refreshed.error ? 200 : 422, {
          ok: refreshed !== null && !refreshed.error,
          ...(refreshed ? { catalog: shownCatalog(refreshed, true) } : {}),
          ...(refreshed?.error ? { error: refreshed.error } : {}),
        });
        return;
      }
      if (action === "groups" && request.method === "GET") {
        json(response, 200, { groups: catalogs.groups(id) ?? [] });
        return;
      }
      if (action === "entries" && entryId === "" && request.method === "GET") {
        const page = catalogs.entries_(id, {
          group: url.searchParams.get("group") ?? "",
          q: url.searchParams.get("q") ?? "",
          offset: Number(url.searchParams.get("offset") ?? "0") || 0,
          limit: Number(url.searchParams.get("limit") ?? "200") || 200,
        }) ?? { total: 0, entries: [] };
        json(response, 200, { total: page.total, entries: page.entries.map(shownEntry) });
        return;
      }

      const entry = action === "entries" && entryId !== "" ? catalogs.entry(id, decodeURIComponent(entryId)) : null;
      if (!entry) {
        json(response, 404, { error: "no such entry" });
        return;
      }

      // Play. A live entry becomes a channel, started for whoever asked and
      // stopped a minute after the last viewer leaves; a film is played on
      // its own, straight from the source through ffmpeg.
      if (sub === "play" && request.method === "POST") {
        if (!entry.live) {
          json(response, 200, {
            kind: "vod",
            url: `/api/catalogs/${encodeURIComponent(id)}/entries/${encodeURIComponent(entry.id)}/stream`,
            name: entry.title,
          });
          return;
        }
        if (!options.channels) {
          json(response, 503, { error: "this server cannot carry channels" });
          return;
        }
        const channelId = cleanId(`cat-${entry.id}`);
        if (!options.channels.has(channelId)) {
          if (options.channels.ephemeralCount >= MAX_ON_DEMAND) {
            json(response, 429, { error: `this server is already carrying ${MAX_ON_DEMAND} channels on demand; try again in a minute` });
            return;
          }
          const started = await pullChannel(
            options.channels, options.ffprobe ?? ["ffprobe"], channelId, entry.title, entry.source, [], "",
            // The logo the list gave it is its picture.
            entry.logo ? { art: entry.logo, about: entry.group } : {},
          );
          if (!started) {
            json(response, 409, { error: "that channel is already starting" });
            return;
          }
          options.channels.ephemeral(channelId);
          // Told to the directory now, so the room code arrives with the
          // channel rather than at the next heartbeat, ninety seconds on.
          void options.live?.announce?.();
        }
        json(response, 200, { kind: "live", channel: channelId, name: entry.title });
        return;
      }

      // Go live with it. The same channel a viewer would get on demand, but
      // kept: it stays up with nobody watching, it is written down so a
      // restart puts it back, and the directory hears about it now. A film
      // goes on the air the same way -- read at its own pace from the start,
      // so everybody who opens the link sees the same minute of it.
      if (sub === "live" && request.method === "POST") {
        if (!options.channels) {
          json(response, 503, { error: "this server cannot carry channels" });
          return;
        }
        const channelId = cleanId(`cat-${entry.id}`);
        // A member's, if a member asked: counted against what they may have
        // on, and theirs to take off. Something already on stays whose it was.
        if (liveBy && !options.channels.has(channelId)) {
          const refusal = memberLiveRefusal(options.channels, liveBy);
          if (refusal) {
            json(response, 429, { error: refusal });
            return;
          }
        }
        if (!options.channels.has(channelId)) {
          const started = await pullChannel(
            options.channels, options.ffprobe ?? ["ffprobe"], channelId, entry.title, entry.source, [], "",
            // The logo the list gave it is its picture.
            entry.logo ? { art: entry.logo, about: entry.group } : {},
          );
          if (!started) {
            json(response, 409, { error: "that channel is already starting" });
            return;
          }
          if (liveBy) {
            const info = options.channels.info(channelId);
            if (info) info.startedBy = liveBy;
          }
        }
        options.channels.keep(channelId);
        options.rememberChannels?.(rememberedNow(options.channels));
        void options.live?.announce?.();
        // Whether there is a picture, so the page can join it in the right
        // element straight away rather than guessing.
        json(response, 200, {
          channel: channelId, name: entry.title, kind: entry.live ? "live" : "vod",
          video: options.channels.kindOf(channelId) !== "audio",
        });
        return;
      }

      if (sub === "stream" && request.method === "GET") {
        if (!options.media) {
          json(response, 403, { error: "media streaming is off" });
          return;
        }
        watch(request, response, "stream", entry.title);
        const codecs = await codecsOf({ ffmpeg: [], ffprobe: options.ffprobe ?? ["ffprobe"], play: null }, entry.source);
        if (codecs.video !== "") {
          pipeFfmpeg(
            request, response, entry.source, options.ffmpeg ?? ["ffmpeg"],
            videoArgs(codecs, 0, { allowHevc: url.searchParams.get("hevc") === "1" }), "video/mp4",
            transportInputArgs(entry.source, codecs.container),
          );
        } else {
          transcode(request, response, entry.source, options.ffmpeg ?? ["ffmpeg"]);
        }
        return;
      }

      json(response, 404, { error: "no such endpoint" });
      return;
    }

    // --- any link, played -------------------------------------------------
    //
    // Paste a page -- YouTube, a podcast, SoundCloud, a TikTok live -- and the
    // server works out where the media is and plays it as a channel of its
    // own, the way a catalog entry is played: started for whoever asked,
    // stopped a minute after the last viewer leaves. Open to anyone holding
    // the link, like picking something from a catalog.
    // --- what is this? ------------------------------------------------------
    //
    // A file name, a playlist entry, a channel: the poster, the logo, the
    // year, the rating, from nichedb.dev, remembered here so a library is
    // asked about once. Open to whoever holds the link, like the playlist.
    if (path === "/api/enrich" && request.method === "GET") {
      const name = (url.searchParams.get("name") ?? "").trim().slice(0, 300);
      if (name === "") {
        json(response, 400, { error: "name is required" });
        return;
      }
      if (!options.enricher) {
        json(response, 200, { match: null });
        return;
      }
      const kinds: EnrichKind[] = ["auto", "title", "channel", "fixture"];
      const asked = url.searchParams.get("kind") ?? "auto";
      const kind = kinds.includes(asked as EnrichKind) ? (asked as EnrichKind) : "auto";
      const year = Number(url.searchParams.get("year")) || null;
      const match = await options.enricher.lookup(name, kind, year);
      response.writeHead(200, {
        ...CORS,
        "content-type": "application/json; charset=utf-8",
        // Briefly: the server remembers for days, so the browser need not,
        // and an hour of browser cache hid a better answer for an hour. A
        // fixture's score moves by the minute and the page asks again every
        // minute; a browser cache that long would answer instead of the server.
        "cache-control": match?.kind === "fixture"
          ? `public, max-age=${Math.floor(FIXTURE_TTL_MS / 2000)}`
          : match ? "public, max-age=300" : "public, max-age=120",
      });
      response.end(JSON.stringify({ match }));
      return;
    }

    /*
     * A link, fetched by this server and carried as a channel.
     *
     * Played: started on demand, stopped a minute after the last viewer
     * leaves. Live (`{ live: true }`, and always for a member): kept,
     * remembered across a restart, listed in the directory with a room
     * code, and -- for a member -- marked theirs and counted against what
     * they may have on at once, exactly like a file from the library.
     */
    if (path === "/api/links/play" && request.method === "POST") {
      let body: { url?: unknown; live?: unknown } = {};
      try {
        body = JSON.parse(await readBody(request)) as typeof body;
      } catch {
        json(response, 400, { error: "bad JSON" });
        return;
      }
      const link = playableLink(body.url);
      if (link === "") {
        json(response, 400, { error: "that is not a link this can play" });
        return;
      }
      if (!options.channels) {
        json(response, 503, { error: "this server cannot carry channels" });
        return;
      }
      if (options.carries === false) {
        json(response, 503, {
          error: `${options.serverName ?? "this server"} has no ffmpeg, so it cannot carry a link. Pick a server to go live on.`,
        });
        return;
      }
      const channelId = linkChannelId(link);
      const known = links.get(link);
      // A member's is always a live: a decoder started on somebody else's
      // machine is theirs to own, not a thing left running anonymously.
      const goLive = body.live === true || liveBy !== "";
      /**
       * On the air and kept, as going live means; for a member, theirs.
       * Answers "" or a refusal. `fresh` is a channel this very request
       * started, which is nobody's yet and was counted before it was.
       */
      const putOnTheAir = (fresh: boolean): string => {
        const channels = options.channels as Channels;
        const info = channels.info(channelId);
        if (!info) return "";
        if (liveBy && !info.startedBy) {
          // Somebody's already, and not started on demand: the owner's.
          if (!fresh && !channels.isEphemeral(channelId)) return "that stream is the owner's";
          if (!fresh) {
            const refusal = memberLiveRefusal(channels, liveBy);
            if (refusal) return refusal;
          }
          info.startedBy = liveBy;
        }
        channels.keep(channelId);
        options.rememberChannels?.(rememberedNow(channels));
        return "";
      };
      if (options.channels.has(channelId) && known) {
        if (goLive) {
          const refusal = putOnTheAir(false);
          if (refusal) {
            json(response, refusal.endsWith("owner's") ? 403 : 429, { error: refusal });
            return;
          }
          void options.live?.announce?.();
        }
        json(response, 200, shownLink(channelId, known, options.channels.info(channelId)));
        return;
      }
      if (!options.channels.has(channelId) && liveBy) {
        const refusal = memberLiveRefusal(options.channels, liveBy);
        if (refusal) {
          json(response, 429, { error: refusal });
          return;
        }
      }
      if (!options.channels.has(channelId) && !goLive && options.channels.ephemeralCount >= MAX_ON_DEMAND) {
        json(response, 429, { error: `this server is already carrying ${MAX_ON_DEMAND} channels on demand; try again in a minute` });
        return;
      }
      // A member's link is fetched from inside the owner's network, so it has
      // to lead somewhere public: the link, every hop of a playlist, and
      // whatever yt-dlp says the media is. The owner's is theirs to aim.
      const guard = liveBy && !known ? (one: string) => refusedDestination(one) : undefined;
      if (guard) {
        const refused = await guard(link);
        if (refused) {
          json(response, 403, { error: refused });
          return;
        }
      }
      const resolved = known ?? await resolveLink(options.ytdlp ?? null, link, { cookies: options.cookies ?? "", guard });
      if ("error" in resolved) {
        json(response, guard && resolved.error.includes("private network") ? 403 : 422, { error: resolved.error });
        return;
      }
      if (guard) {
        const reached = [resolved.media, resolved.audio, ...(resolved.playlist ?? [])].filter((one) => one && /^(https?|rtmps?):\/\//i.test(one));
        for (const one of [...new Set(reached)].slice(0, 200)) {
          const refused = await guard(one);
          if (refused) {
            json(response, 403, { error: refused });
            return;
          }
        }
      }
      links.set(link, resolved);
      // A channel already on -- put back by a restart, which forgets where
      // it came from -- learns its picture and its line from the link pasted
      // again, rather than waiting to be taken off and put on.
      const already = options.channels.info(channelId);
      if (already) {
        if (!already.art && resolved.thumbnail) already.art = resolved.thumbnail;
        if (!already.about && resolved.about) already.about = resolved.about;
        if (already.art || already.about) options.rememberChannels?.(rememberedNow(options.channels));
      }
      if (!options.channels.has(channelId)) {
        // Asked of the site first, once, with the headers yt-dlp said to
        // send. A media address that answers nothing is a site refusing
        // this server -- YouTube does, for many videos, to a datacenter --
        // and the honest answer is that, now, rather than a channel that
        // starts, gets 403 five times, and quietly stops.
        const reachable = await codecsOf(
          { ffmpeg: [], ffprobe: options.ffprobe ?? ["ffprobe"], play: null }, resolved.media, inputArgsFor(resolved.headers),
        );
        if (reachable.video === "" && reachable.audio === "") {
          links.delete(link);
          json(response, 422, {
            error: `${resolved.extractor || "the site"} would not hand this server the media (often a sign-in or a bot check on a datacenter address). It plays in a browser instead.`,
          });
          return;
        }
        const started = await pullChannel(
          options.channels, options.ffprobe ?? ["ffprobe"], channelId, resolved.title, resolved.media,
          inputArgsFor(resolved.headers), resolved.audio,
          {
            ...(resolved.playlist ? { playlist: resolved.playlist } : {}),
            ...(resolved.thumbnail ? { art: resolved.thumbnail } : {}),
            ...(resolved.about ? { about: resolved.about } : {}),
          },
        );
        if (!started) {
          json(response, 409, { error: "that link is already starting" });
          return;
        }
        if (goLive) putOnTheAir(true);
        else options.channels.ephemeral(channelId);
        // Told to the directory now, so the room code arrives with the
        // channel rather than at the next heartbeat, ninety seconds on.
        void options.live?.announce?.();
      } else if (goLive) {
        const refusal = putOnTheAir(false);
        if (refusal) {
          json(response, refusal.endsWith("owner's") ? 403 : 429, { error: refusal });
          return;
        }
        void options.live?.announce?.();
      }
      json(response, 200, shownLink(channelId, resolved, options.channels.info(channelId)));
      return;
    }

    // The whole thing, to keep. The server fetches it through yt-dlp and
    // hands the bytes straight on as a download, so the person's own machine
    // ends up with the file and the server keeps nothing. A live stream has
    // no whole to hand over.
    if (path === "/api/links/download") {
      const link = playableLink(url.searchParams.get("url"));
      if (link === "") {
        json(response, 400, { error: "that is not a link this can fetch" });
        return;
      }
      if (!options.ytdlp || options.ytdlp.length === 0) {
        json(response, 503, { error: "this server has no yt-dlp to fetch with" });
        return;
      }
      const resolved = links.get(link) ?? await resolveLink(options.ytdlp, link, { cookies: options.cookies ?? "" });
      if ("error" in resolved) {
        json(response, 422, { error: resolved.error });
        return;
      }
      links.set(link, resolved);
      if (resolved.live) {
        json(response, 409, { error: "that is live; there is no whole file to download yet" });
        return;
      }
      const audioOnly = url.searchParams.get("audio") === "1" || !resolved.video;
      // Named for the format the download will actually take, which is not
      // always the one played: a track played from an HLS playlist is saved
      // as the plain MP3 the site also offers, and ".m4a" on an MP3 is a
      // file nothing will open.
      const saved = await resolveLink(options.ytdlp, link, { cookies: options.cookies ?? "", format: saveFormat(audioOnly) });
      const chosen = "error" in saved ? resolved : { ...resolved, ext: saved.ext || resolved.ext, media: saved.media, audio: saved.audio, headers: saved.headers };
      const fileName = fileNameFor(chosen, audioOnly);
      // A picture and a sound kept apart by the site are put together here
      // by ffmpeg, as they go: yt-dlp only merges into a file it can seek
      // in, which a pipe is not. Anything else yt-dlp hands over whole.
      const paired = !audioOnly && chosen.audio !== "";
      const [command, ...prefix] = (paired ? (options.ffmpeg ?? ["ffmpeg"]) : options.ytdlp) as [string, ...string[]];
      const child = spawn(
        command,
        [...prefix, ...(paired ? mergeDownloadArgs(chosen) : downloadArgs(link, audioOnly, options.cookies ?? ""))],
        { stdio: ["ignore", "pipe", "pipe"] },
      );
      response.writeHead(200, {
        "content-type": contentTypeFor(fileName),
        "content-disposition": `attachment; filename="${fileName.replace(/["\\]/g, "")}"; filename*=UTF-8''${encodeURIComponent(fileName)}`,
        "cache-control": "no-store",
      });
      child.stdout?.pipe(response);
      let complaint = "";
      child.stderr?.on("data", (chunk: Buffer) => {
        if (complaint.length < 20_000) complaint += chunk.toString("utf8");
      });
      child.on("error", () => response.end());
      child.on("close", (code) => {
        if (code !== 0) console.log(`  a download of ${link} failed: ${complaint.split("\n").filter((one) => one.startsWith("ERROR")).pop() ?? code}`);
        response.end();
      });
      // The person closed the tab: stop fetching what nobody will keep.
      response.on("close", () => {
        if (child.exitCode === null) child.kill("SIGKILL");
      });
      return;
    }

    /**
     * A file on this server, on the air as a channel of its own.
     *
     * The same act as going live with a catalog entry, for a file in the
     * library: its own room, its own phone code, kept, remembered, listed.
     * It used to take over the server's one player, which a member must not
     * do and which made two files at once impossible for anybody. The owner
     * and any member may; the caps and the throttle above say how much.
     */
    if (path === "/api/folders/live" && request.method === "POST") {
      if (!options.channels) {
        json(response, 503, { error: "this server cannot carry channels" });
        return;
      }
      let body: { indices?: unknown; name?: unknown } = {};
      try { body = JSON.parse(await readBody(request)) as typeof body; } catch {
        json(response, 400, { error: "bad JSON" });
        return;
      }
      const indices = Array.isArray(body.indices)
        ? [...new Set(body.indices.filter((one): one is number => Number.isInteger(one) && one >= 0))]
        : [];
      if (indices.length === 0 || indices.length > 10_000) {
        json(response, 400, { error: "choose a folder with playable files" });
        return;
      }
      const tracks = engine.snapshot().tracks ?? [];
      const selected = indices
        .map((index) => ({ index, track: tracks[index], source: engine.trackPath(index) }))
        .filter((one): one is { index: number; track: (typeof tracks)[number]; source: string } => Boolean(one.track && one.source && !isRemote(one.source)));
      if (selected.length !== indices.length) {
        json(response, 404, { error: "one or more folder files are no longer available" });
        return;
      }
      // `indices` already follows the server's depth-first library order.
      // Keep it intact: sorting these paths by title flattens nested folders
      // and makes a folder live start the wrong file.
      const sources = selected.map((one) => one.source);
      const channelId = cleanId(`folder-${sha("sha1").update(sources.join("\n")).digest("hex").slice(0, 12)}`);
      const name = typeof body.name === "string" && body.name.trim() !== ""
        ? body.name.trim().slice(0, 120)
        : selected[0]?.track.folder || "folder";
      if (liveBy && !options.channels.has(channelId)) {
        const refusal = memberLiveRefusal(options.channels, liveBy);
        if (refusal) { json(response, 429, { error: refusal }); return; }
      }
      if (!options.channels.has(channelId)) {
        const started = await pullChannel(options.channels, options.ffprobe ?? ["ffprobe"], channelId, name, sources[0]!, [], "", { playlist: sources, live: true });
        if (!started) { json(response, 409, { error: "that folder is already going on the air" }); return; }
        if (liveBy) options.channels.info(channelId)!.startedBy = liveBy;
      }
      options.channels.keep(channelId);
      options.rememberChannels?.(rememberedNow(options.channels));
      void options.live?.announce?.();
      json(response, 200, { channel: channelId, name, video: options.channels.kindOf(channelId) !== "audio" });
      return;
    }

    const trackLive = /^\/api\/tracks\/(\d+)\/live$/.exec(path);
    if (trackLive && request.method === "POST") {
      if (!options.channels) {
        json(response, 503, { error: "this server cannot carry channels" });
        return;
      }
      const index = Number(trackLive[1]);
      const source = engine.trackPath(index);
      if (!source || isRemote(source)) {
        json(response, 404, { error: "no such file on this server" });
        return;
      }
      const name = engine.snapshot().tracks?.[index]?.title || source.split("/").pop() || `track ${index + 1}`;
      // Named for the file, so the same file is the same channel whoever asks.
      const channelId = cleanId(`file-${sha("sha1").update(source).digest("hex").slice(0, 12)}`);
      if (liveBy && !options.channels.has(channelId)) {
        const refusal = memberLiveRefusal(options.channels, liveBy);
        if (refusal) {
          json(response, 429, { error: refusal });
          return;
        }
      }
      if (!options.channels.has(channelId)) {
        const started = await pullChannel(options.channels, options.ffprobe ?? ["ffprobe"], channelId, name, source);
        if (!started) {
          json(response, 409, { error: "that file is already going on the air" });
          return;
        }
        if (liveBy) {
          const info = options.channels.info(channelId);
          if (info) info.startedBy = liveBy;
        }
      }
      options.channels.keep(channelId);
      options.rememberChannels?.(rememberedNow(options.channels));
      void options.live?.announce?.();
      json(response, 200, { channel: channelId, name, video: options.channels.kindOf(channelId) !== "audio" });
      return;
    }

    // A channel is one publisher and everybody listening to them. Two or three
    // devices can publish at once, each to their own channel, and a listener
    // picks which to hear.
    // The server's compression at a glance, its switch, and the analyses.
    if (path.startsWith("/api/compression")) {
      const compression = compressionCtx();
      if (compression && (await handleCompressionApi(request, response, compression, path))) return;
    }

    if (path === "/api/channels" && options.channels) {
      // Without the source. Anyone holding the listen link may ask what is
      // on, and the address a channel is pulled from is the one thing about
      // it that is not theirs to have.
      json(response, 200, {
        // Stations answer for themselves, under /api/v1/stations.
        channels: options.channels.list().filter((one) => !stationOfChannel(one.id)).map(({ source: _source, ...shown }) => shown),
        listeners: options.channels.listeners,
      });
      return;
    }

    // Publishing. Anyone with the control link may; listening to the result is
    // open to whoever has the share link, like the rest of the audio.
    if (path.startsWith("/api/channels/") && options.channels) {
      const channels = options.channels;
      const rest = path.slice("/api/channels/".length);
      const [rawId, action, file] = rest.split("/");
      const id = cleanId(rawId);
      // A station's channel is heard through the station's own link, which
      // knows whether it is private. Through here, only by its owner.
      if (stationOfChannel(id) && !(await controlOf()).allowed) {
        json(response, 404, { error: "nothing is playing on that channel" });
        return;
      }
      const event = await eventForChannel();
      if (event) {
        const account = await options.accounts?.whoIs(tokenFrom(request.headers)) ?? null;
        if (request.method === "GET") {
          const invited = await options.events!.invitationAllows(event.id, url.searchParams.get("invite") ?? "");
          if (!(await options.events!.canAccess(event, account?.id)) && !invited) {
            json(response, 404, { error: "nothing is playing on that channel" });
            return;
          }
          // A ticketed room is the one place a 402 belongs on the media
          // itself: there is a thing to buy, it is this, and the answer says
          // what it costs rather than pretending the room is not there.
          if (isTicketed(event)) {
            const held = await options.tickets?.holds(event, ticketFrom(request.headers, url)) ?? false;
            if (needsTicket(event, account?.id, held)) {
              json(response, 402, {
                error: `${event.title} is a ticketed show.`,
                ticket: options.tickets?.offer(event) ?? {
                  ticketed: true,
                  priceCents: event.ticketPriceCents,
                  currency: event.ticketCurrency,
                },
              });
              return;
            }
          }
        } else if (!options.events!.canPerform(event, account?.id)) {
          json(response, account ? 403 : 401, { error: account ? "only the people on this stage can publish here" : "sign in to host this event" });
          return;
        } else if (request.method === "POST" &&
                   (action === undefined || action === "chunk") &&
                   event.status !== "live" && event.status !== "encore") {
          json(response, 409, { error: "start the event before publishing audio" });
          return;
        }
      }

      // Its compression policy, an analysis of it, and its relay to or from
      // another nixamp. The ordinary playback URL above is untouched by any
      // of it: a relay is a different media type on a different path.
      const compression = compressionCtx();
      if (compression && (await handleChannelCompression(request, response, compression, id, action, file))) return;

      // A picture of the channel: the sleeve in the file, or one frame of
      // what it is sending, as a JPEG. This is what a link preview shows
      // and what the lock screen shows, and a crawler fetches it with the
      // listen key on the address, since it holds no cookie.
      if (action === "art" && (request.method === "GET" || request.method === "HEAD")) {
        const info = channels.info(id);
        if (!info || options.carries === false) {
          json(response, 404, { error: "nothing is playing on that channel" });
          return;
        }
        // The site's own picture is where it is; sent there, so one address
        // per channel serves whichever kind it has.
        if (info.art && /^https?:\/\//i.test(info.art)) {
          response.writeHead(302, { ...CORS, location: info.art, "cache-control": "public, max-age=300" });
          response.end();
          return;
        }
        const ffmpeg = options.ffmpeg ?? ["ffmpeg"];
        const cover = info.codecs?.cover === true && Boolean(info.source);
        const cache = options.art ?? new ArtCache();
        const bytes = cover
          ? await cache.get(`${id}:cover:${info.startedAt}`, true, () => {
              // With the headers its site wanted, when it came from a link.
              const link = [...links.values()].find((one) => one.media === info.source);
              return coverArtOf(ffmpeg, info.source as string, link ? inputArgsFor(link.headers) : []);
            })
          : info.kind === "video"
            ? await cache.get(`${id}:still`, false, () => stillFrom(ffmpeg, channels.opening(id)))
            : null;
        if (!bytes) {
          json(response, 404, { error: "that channel has no picture yet" });
          return;
        }
        response.writeHead(200, {
          ...CORS,
          "content-type": "image/jpeg",
          "content-length": bytes.byteLength,
          "cache-control": cover ? "public, max-age=3600" : "public, max-age=60",
        });
        response.end(request.method === "HEAD" ? undefined : bytes);
        return;
      }

      // A station asleep with nobody listening is woken by whoever asks for
      // its channel here too, and they are held for its first bytes.
      if ((action === "hls" || action === undefined) && request.method === "GET" && !channels.has(id) && stationOfChannel(id) && options.stations?.runner.isSleeping(stationOfChannel(id))) {
        await options.stations.runner.awake(stationOfChannel(id));
      }

      // The same channel as HLS: a playlist of short files, which is what
      // Safari on an iPhone plays live -- it will not take the endless MP4
      // below, and spun on it a few times before giving up. Packaged on
      // demand, copying the fragments the browser would have got.
      if (action === "hls" && request.method === "GET") {
        if (!options.hls) {
          json(response, 503, { error: "this server cannot package HLS" });
          return;
        }
        if (!channels.has(id)) {
          json(response, 404, { error: "nothing is playing on that channel" });
          return;
        }
        if (file === "index.m3u8") {
          // A channel carrying H.265 is cut into fMP4 rather than transport
          // segments: HLS in TS is defined for H.264 only, and Safari plays
          // an HEVC channel packaged as TS as sound over a black screen.
          const playlist = await options.hls.playlist(id, channels.info(id)?.emits === "hevc");
          if (playlist === null) {
            json(response, 503, { error: "that channel could not be packaged as HLS yet; try again in a moment" });
            return;
          }
          response.writeHead(200, {
            ...CORS,
            "content-type": "application/vnd.apple.mpegurl",
            "cache-control": "no-store",
          });
          // The key rides on every segment line: a browser drops the query
          // when it resolves a segment against the playlist.
          response.end(withKey(playlist, url.searchParams.get("k") ?? ""));
          return;
        }
        const segment = options.hls.segment(id, file ?? "");
        if (segment === "") {
          json(response, 404, { error: "no such segment" });
          return;
        }
        watch(request, response, "stream", id);
        response.writeHead(200, {
          ...CORS,
          // By its name: a TS segment, an fMP4 media segment, or the init.
          "content-type": segmentType(file ?? ""),
          "cache-control": "no-store",
          "content-length": statSync(segment).size,
        });
        createReadStream(segment).pipe(response);
        return;
      }

      /*
       * The channel's captions: what it is saying, as lines, as they are
       * heard by nixamp.com's ear on this server's behalf. Read the way the
       * sound is read -- the key gate above has already passed -- and
       * started by the first person asking. `captions` is the live stream
       * of lines; `transcript` is the recent ones as JSON, for a poll.
       */
      if ((action === "voice" || action === "voice-options") && request.method === "GET") {
        const controller = new AbortController();
        response.once("close", () => controller.abort());
        const timeout = setTimeout(() => controller.abort(), 12_000);
        try {
          const caller = callerOf(request.headers, request.socket.remoteAddress, options.behindProxy ?? false);
          if (!guard.check(`channel-voice-ip:${caller}`, { allowed: 60, windowMs: 60_000 }).ok || !guard.check(`channel-voice:${id}`, { allowed: 240, windowMs: 60_000 }).ok) {
            json(response, 429, { error: "too many translated audio requests" }); return;
          }
          if (!channels.has(id)) { json(response, 404, { error: "nothing is playing on that channel" }); return; }
          if (!options.captions) { json(response, 503, { error: "this server cannot caption" }); return; }
          const at = action === "voice-options" ? null : Number(url.searchParams.get("at"));
          const language = languageCode(url.searchParams.get("language"));
          if ((at !== null && (!url.searchParams.has("at") || !Number.isFinite(at))) || language === null) {
            json(response, 400, { error: "audio needs a caption time and language" }); return;
          }
          const upstream = await options.captions.voiceRequest(id, at, language, url.searchParams.get("voice") ?? "auto", controller.signal, tokenFrom(request.headers));
          await voiceResponse(response, upstream);
        } catch (error) {
          if (response.headersSent) response.destroy();
          else json(response, error instanceof SpeechError ? error.status : 502, { error: error instanceof SpeechError ? error.message : "translated audio is unavailable" });
        } finally { clearTimeout(timeout); }
        return;
      }

      if ((action === "captions" || action === "transcript") && request.method === "GET") {
        const captions = options.captions;
        if (!captions) {
          json(response, 503, { error: "this server cannot caption: it has no ffmpeg" });
          return;
        }
        if (!captions.available()) {
          json(response, 503, { error: "this server is not signed in to nixamp.com, so it cannot caption; run `nixamp login` on it" });
          return;
        }
        const caller = callerOf(request.headers, request.socket.remoteAddress, options.behindProxy ?? false);
        if (!guard.check(`captions:${caller}`, { allowed: 120, windowMs: 60_000 }).ok || !captions.capacity(id)) {
          json(response, 429, { error: "captioning is at capacity; try again in a moment" });
          return;
        }
        if (!channels.has(id)) {
          json(response, 404, { error: "nothing is playing on that channel" });
          return;
        }
        // Lines after a moment: a poll's ?after=, or the last line a
        // reconnecting EventSource saw, which the browser sends by itself.
        const lastSeen = request.headers["last-event-id"];
        const after = Number(url.searchParams.get("after") ?? (Array.isArray(lastSeen) ? lastSeen[0] : lastSeen) ?? 0) || 0;
        // The lines in another language, translated as they are heard; "" is what was said.
        const wanted = languageCode(url.searchParams.get("language"));
        if (wanted === null) {
          json(response, 400, { error: "language is a two-letter code, such as de or sv" });
          return;
        }
        if (action === "transcript") {
          // Asking keeps the captioner up: it stops a minute after the last ask.
          captions.subscribe(id, () => undefined, wanted)?.();
          json(response, 200, {
            channel: id, recognitionRevision: NATIVE_REVISION, backlog: BACKLOG_SECONDS, now: Date.now(), wanted, ...captions.status(id), lines: captions.recent(id, after, wanted),
          });
          return;
        }
        response.writeHead(200, {
          ...CORS,
          "content-type": "text/event-stream; charset=utf-8",
          "cache-control": "no-store",
          connection: "keep-alive",
          "x-accel-buffering": "no",
        });
        const write = (event: string, data: unknown, eventId?: number): void => {
          response.write(`${eventId === undefined ? "" : `id: ${eventId}\n`}event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
        };
        const off = captions.subscribe(id, (line) => write("line", line, line.at), wanted);
        if (off === null) {
          response.end();
          return;
        }
        // How far behind the live edge a newcomer's playback starts, so the
        // page can hold each line until its own sound gets there.
        write("hello", { channel: id, recognitionRevision: NATIVE_REVISION, backlog: BACKLOG_SECONDS, now: Date.now(), wanted, ...captions.status(id), lines: captions.recent(id, after, wanted) });
        const beat = setInterval(() => response.write(": beat\n\n"), 20_000);
        beat.unref?.();
        const done = (): void => {
          clearInterval(beat);
          off();
        };
        request.on("close", done);
        response.on("close", done);
        return;
      }

      if (action === undefined && request.method === "GET") {
        // Listening. The response is the fan-out target: whatever ffmpeg
        // produces for this channel is written to it until one end goes away.
        if (!channels.has(id)) {
          json(response, 404, { error: "nothing is playing on that channel" });
          return;
        }
        watch(request, response, "stream", id);
        // Headers first, and then the listener.
        //
        // Attaching first was fine while a channel only ever wrote future
        // bytes. A video channel writes the opening boxes to a new listener
        // the moment it joins, and those went out before this response had
        // any headers at all -- so it committed as a bare 200 with no
        // content-type, ended immediately, and the picture was one kilobyte
        // long. Whether it happened depended on whether ffmpeg had produced
        // its header yet, which is why it looked intermittent.
        response.writeHead(200, {
          ...CORS,
          // Asked of the channel rather than assumed: a channel carrying
          // pictures that calls itself audio/mpeg plays as nothing at all.
          "content-type": channels.contentType(id),
          "cache-control": "no-store",
        });
        const detach = channels.listen(id, response);
        if (detach === null) {
          response.end();
          return;
        }
        const leave = (): void => detach();
        request.on("close", leave);
        response.on("close", leave);
        return;
      }

      /*
       * Rename what is on the air. A link goes on as "906" or whatever the
       * file was called, and the directory lists that; the person who put it
       * on knows what it is. The owner renames anything; a member, their own.
       */
      if (action === undefined && request.method === "PATCH") {
        const info = channels.info(id);
        if (!info) {
          json(response, 404, { error: "nothing is playing on that channel" });
          return;
        }
        if (liveBy && info.startedBy !== liveBy) {
          json(response, 403, { error: "that stream is not yours to rename" });
          return;
        }
        let body: { name?: unknown } = {};
        try {
          body = JSON.parse(await readBody(request)) as typeof body;
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        const name = typeof body.name === "string" ? body.name.replace(/[\r\n\t]+/g, " ").trim().slice(0, 120) : "";
        if (name === "") {
          json(response, 400, { error: "a name is needed" });
          return;
        }
        info.name = name;
        options.rememberChannels?.(rememberedNow(channels));
        // Listed under the new name now, not at the next heartbeat.
        void options.live?.announce?.();
        json(response, 200, { ok: true, channel: id, name });
        return;
      }

      if ((action === undefined && request.method === "DELETE") || (action === "stop" && request.method === "POST")) {
        // A member takes off what they put on, and nothing else.
        if (liveBy && channels.info(id)?.startedBy !== liveBy) {
          json(response, 403, { error: "that stream is not yours to take off" });
          return;
        }
        // A channel fed by another nixamp's relay is taken off by stopping
        // the relay, which takes the channel with it; otherwise it would
        // dial again the moment the channel went. Asked once either way:
        // the second call would answer false, having just stopped the
        // thing it was asking about.
        const stopped = (options.compression?.stopPull(id) ?? false) || channels.stop(id);
        // Taken off on purpose is forgotten on purpose: it must not come back
        // at the next restart.
        if (stopped) {
          options.rememberChannels?.(rememberedNow(channels));
          // Off the air is news too: told now, so the directory drops it
          // rather than listing it until the next heartbeat.
          void options.live?.announce?.();
          if (event && (event.status === "live" || event.status === "encore")) {
            await options.events?.transition(event.id, event.ownerId, "ended", event.version).catch(() => undefined);
          }
        }
        json(response, stopped ? 200 : 404, { ok: stopped });
        return;
      }

      if (request.method !== "POST") {
        json(response, 405, { error: "GET, POST or DELETE" });
        return;
      }

      /**
       * Dial the source again, now.
       *
       * The thing an administrator reaches for when a channel says it is on
       * the air and shows nobody anything. It is what fixed CNN by hand --
       * take it off, put it back -- without having to know the source, which
       * a browser is never told.
       */
      if (action === "start" || action === "restart") {
        if (!channels.has(id)) {
          json(response, 404, { error: "nothing is playing on that channel" });
          return;
        }
        if (!channels.pulled(id)) {
          json(response, 409, { error: "that channel is published into this server; restart it at the publisher" });
          return;
        }
        if (liveBy && channels.info(id)?.startedBy !== liveBy) {
          json(response, 403, { error: "that stream is not yours to restart" });
          return;
        }
        const restarted = channels.restart(id);
        json(response, restarted ? 200 : 409, { ok: restarted });
        return;
      }

      /*
       * Keep a channel that was started on demand. Something being watched
       * from a catalog stops a minute after its last viewer leaves; going
       * live with it is asking it not to, and asking the directory to list
       * it now.
       */
      if (action === "keep") {
        if (!channels.has(id)) {
          json(response, 404, { error: "nothing is playing on that channel" });
          return;
        }
        // Keeping something started on demand is putting it on the air: for
        // a member, counted and marked like anything else they put on.
        if (liveBy) {
          const info = channels.info(id);
          if (info && info.startedBy && info.startedBy !== liveBy) {
            json(response, 403, { error: "that stream is not yours to keep" });
            return;
          }
          if (info && !info.startedBy && !channels.isEphemeral(id)) {
            json(response, 403, { error: "that stream is the owner's" });
            return;
          }
          if (info && !info.startedBy) {
            const refusal = memberLiveRefusal(channels, liveBy);
            if (refusal) {
              json(response, 429, { error: refusal });
              return;
            }
            info.startedBy = liveBy;
          }
        }
        channels.keep(id);
        options.rememberChannels?.(rememberedNow(channels));
        void options.live?.announce?.();
        json(response, 200, { ok: true });
        return;
      }

      /**
       * Carry a source of our own, rather than waiting to be sent one.
       *
       * A re-stream used to be added to the playlist, where it became one
       * more track -- and a server plays one track at a time, so the second
       * channel you added sat there saying "stopped". Two channels are two
       * processes with two audiences and two addresses, which is what lets
       * one person watch the baseball while another watches the news, in two
       * tabs or in two panels of the same multiview.
       */
      if (action === "pull") {
        let source = "";
        let called = "";
        try {
          const body = JSON.parse(await readBody(request)) as {
            source?: unknown; name?: unknown; at?: unknown;
          };
          called = String(body.name ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 80);
          // A track number rather than a path: it is the server's own library
          // either way, and a number cannot name a file outside it.
          if (typeof body.at === "number" && Number.isInteger(body.at) && body.at >= 0) {
            source = engine.trackPath(body.at) ?? "";
            if (source === "") {
              json(response, 404, { error: "no track there" });
              return;
            }
          } else {
            source = String(body.source ?? "").trim();
          }
        } catch {
          json(response, 400, { error: "bad JSON" });
          return;
        }
        if (source === "") {
          json(response, 400, { error: "give a URL to carry, or a track to show" });
          return;
        }

        const wanted = cleanId(rawId, generatedId());
        if (channels.has(wanted)) {
          json(response, 409, { error: "that channel is already on" });
          return;
        }

        const channel = await pullChannel(channels, options.ffprobe ?? ["ffprobe"], wanted, called, source);
        if (!channel) {
          json(response, 409, { error: "that channel is already on" });
          return;
        }
        // Written down, so a restart puts it back on the air.
        options.rememberChannels?.(rememberedNow(channels));
        json(response, 200, { ok: true, channel: channel.info });
        return;
      }

      const format = normaliseFormat(url.searchParams.get("format") ?? request.headers["content-type"]);
      if (format === null) {
        json(response, 415, { error: "give a container ffmpeg knows: webm, ogg, mp4, mp3, wav" });
        return;
      }
      const name = url.searchParams.get("name") ?? "";

      // A chunked publisher sends many requests to one channel, so the first
      // claims it and the rest feed what is already there.
      if (action === "chunk") {
        if (!channels.has(id) && channels.publish(id, name, format, "http") === null) {
          json(response, 409, { error: "that channel is already being published to" });
          return;
        }
        const chunks: Buffer[] = [];
        for await (const chunk of request) chunks.push(chunk as Buffer);
        channels.writeTo(id, Buffer.concat(chunks));
        json(response, 200, { ok: true });
        return;
      }

      const claimed = channels.publish(id, name, format, "http");
      if (claimed === null) {
        json(response, 409, { error: "that channel is already being published to" });
        return;
      }
      try {
        await claimed.pump(request);
      } catch {
        // A publisher that hung up is not an error worth a 500.
      }
      claimed.close();
      json(response, 200, { ok: true, bytes: claimed.info.bytes });
      return;
    }

    // --- streaming in ---------------------------------------------------
    //
    // Both shapes write into the same ffmpeg, so everything downstream cannot
    // tell which one a sender used.
    if (path === "/api/ingest" && options.ingest) {
      if (request.method === "GET") {
        json(response, 200, options.ingest.status());
        return;
      }
      if (request.method === "DELETE") {
        options.ingest.close();
        json(response, 200, { ok: true });
        return;
      }
      if (request.method !== "POST") {
        json(response, 405, { error: "GET, POST or DELETE" });
        return;
      }

      const format = normaliseFormat(url.searchParams.get("format") ?? request.headers["content-type"]);
      if (format === null) {
        json(response, 415, { error: "give a container ffmpeg knows: webm, ogg, mp4, mp3, wav" });
        return;
      }

      const session = options.ingest.open(url.searchParams.get("name") ?? "", format);
      if (session === null) {
        // Somebody else is already broadcasting, which is a different problem
        // from the request being wrong.
        json(response, 409, { error: "something is already streaming in" });
        return;
      }

      try {
        await options.ingest.pump(request);
      } catch {
        // A sender that hung up is not an error worth a 500.
      }
      options.ingest.close();
      json(response, 200, { ok: true, bytes: session.bytes });
      return;
    }

    // A browser cannot stream a request body over plain HTTP/1.1, so a phone
    // sends its recording a chunk at a time instead.
    if (path === "/api/ingest/chunk" && options.ingest) {
      if (request.method !== "POST") {
        json(response, 405, { error: "POST only" });
        return;
      }
      if (!options.ingest.live) {
        const format = normaliseFormat(url.searchParams.get("format") ?? request.headers["content-type"]);
        if (format === null) {
          json(response, 415, { error: "give a container ffmpeg knows: webm, ogg, mp4, mp3, wav" });
          return;
        }
        if (options.ingest.open(url.searchParams.get("name") ?? "", format) === null) {
          json(response, 409, { error: "something is already streaming in" });
          return;
        }
      }
      const chunks: Buffer[] = [];
      for await (const chunk of request) chunks.push(chunk as Buffer);
      options.ingest.write(Buffer.concat(chunks));
      json(response, 200, options.ingest.status());
      return;
    }

    // --- broadcasting out -------------------------------------------------
    if (path === "/api/broadcast" && options.broadcaster) {
      if (request.method === "GET") {
        json(response, 200, options.broadcaster.status());
        return;
      }
      if (request.method === "DELETE") {
        options.broadcaster.stop();
        json(response, 200, options.broadcaster.status());
        return;
      }
      if (request.method !== "POST") {
        json(response, 405, { error: "GET, POST or DELETE" });
        return;
      }

      let source = "";
      try {
        source = String((JSON.parse(await readBody(request)) as { source?: unknown }).source ?? "");
      } catch {
        json(response, 400, { error: "bad JSON" });
        return;
      }
      const current = engine.snapshot();
      const chosen = source || engine.trackPath(current.index) || "";
      if (!chosen) {
        json(response, 422, { error: "nothing to broadcast" });
        return;
      }

      const plan = options.broadcast?.() ?? { destinations: [], settings: DEFAULT_ENCODER };
      const started = options.broadcaster.start({
        source: chosen,
        destinations: plan.destinations,
        settings: plan.settings,
        webAudio: false,
        // Music has no picture, and RTMP platforms insist on a video track.
        needsVideo: true,
      });
      if (!started.ok) {
        json(response, 422, { error: started.error });
        return;
      }
      json(response, 200, options.broadcaster.status());
      return;
    }

    // Names and URLs, never a key.
    if (path === "/api/broadcast/destinations" && options.broadcast) {
      json(response, 200, { destinations: options.broadcast().destinations.map(redact) });
      return;
    }

    if (path === "/api/state") {
      json(response, 200, listening ? { ...engine.snapshot(), root: "" } : engine.snapshot());
      return;
    }

    // Everything the admin view draws, in one request: who is connected, and
    // what this server is.
    // Going live, and coming back off. Admin-gated by ADMIN_PATHS, because
    // listing somebody's machine in a public directory is not a thing a
    // listener gets to do.
    if (path === "/api/live/state") {
      if (!options.live) {
        json(response, 200, { live: false, possible: false, code: "", name: "", url: "" });
        return;
      }
      json(response, 200, options.live.status());
      return;
    }

    if (path === "/api/live/start" || path === "/api/live/stop") {
      if (request.method !== "POST") {
        json(response, 405, { error: "POST only" });
        return;
      }
      if (!options.live) {
        json(response, 409, { error: "this nixamp cannot be listed; it has no address the world can reach" });
        return;
      }
      if (path === "/api/live/stop") {
        await options.live.stop();
        json(response, 200, options.live.status());
        return;
      }
      const started = await options.live.start();
      json(response, started.error ? 502 : 200, started);
      return;
    }

    if (path === "/api/connections") {
      json(response, 200, {
        connections: tracker.list(),
        active: tracker.active,
        startedAt: started,
        now: Date.now(),
        // Where to point OBS. Printed at startup since RTMP was added, which
        // is no use at all to somebody looking at the admin panel a day later.
        publish: options.publishUrls?.() ?? [],
        // And which of those slots somebody is already on, because the
        // question you have in front of three addresses is which one is free.
        channels: options.channels?.list().map(({ id, name, via }) => ({ id, name, via })) ?? [],
        // What this server's own library is, and whether it is loaded, so
        // there can be a way back to it that is not retyping a path.
        home: options.homeSource ?? "",
        root: engine.snapshot(false).root,
      });
      return;
    }

    if (path === "/api/events") {
      watch(request, response, "events", "");
      response.writeHead(200, {
        ...CORS,
        "content-type": "text/event-stream; charset=utf-8",
        "cache-control": "no-store",
        connection: "keep-alive",
        // nginx and friends buffer text/event-stream into uselessness.
        "x-accel-buffering": "no",
      });
      const send = (snapshot: Snapshot): void => {
        response.write(`data: ${JSON.stringify(listening ? { ...snapshot, root: "" } : snapshot)}\n\n`);
      };
      const unsubscribe = engine.subscribe(send);
      // A comment line keeps proxies from closing an idle stream.
      const beat = setInterval(() => response.write(": beat\n\n"), 20_000);
      beat.unref?.();
      const done = (): void => {
        clearInterval(beat);
        unsubscribe();
      };
      request.on("close", done);
      response.on("close", done);
      return;
    }

    if (path === "/api/command") {
      if (request.method !== "POST") {
        json(response, 405, { error: "POST only" });
        return;
      }
      let parsed: unknown;
      try {
        parsed = JSON.parse(await readBody(request)) as unknown;
      } catch {
        json(response, 400, { error: "bad JSON" });
        return;
      }
      const command = parseCommand(parsed);
      if (!command) {
        json(response, 400, { error: "unknown command" });
        return;
      }
      engine.command(command);
      json(response, 200, engine.snapshot());
      return;
    }

    // Take an added source back out of the playlist. The library it was added
    // to is untouched -- there is no group name that names it.
    if (path === "/api/source/remove") {
      if (request.method !== "POST") {
        json(response, 405, { error: "POST only" });
        return;
      }
      let group = "";
      try {
        group = String((JSON.parse(await readBody(request)) as { group?: unknown }).group ?? "");
      } catch {
        json(response, 400, { error: "bad JSON" });
        return;
      }
      if (!group) {
        json(response, 400, { error: "no group given" });
        return;
      }
      const removed = engine.drop(group);
      if (removed === 0) {
        json(response, 404, { error: `nothing here came from ${group}` });
        return;
      }
      json(response, 200, { ...engine.snapshot(), removed, groups: engine.groups() });
      return;
    }

    // Hand the running server another source. The listeners stay connected;
    // by default they get more to listen to, and only an explicit `replace`
    // swaps what this server is for something else.
    if (path === "/api/source") {
      if (request.method !== "POST") {
        json(response, 405, { error: "POST only" });
        return;
      }
      let source = "";
      let called = "";
      let replacing = false;
      try {
        const body = JSON.parse(await readBody(request)) as {
          source?: unknown; replace?: unknown; name?: unknown;
        };
        source = String(body.source ?? "");
        // Trimmed and capped: it is a label, and it can end up in a listing.
        called = String(body.name ?? "").replace(/[\u0000-\u001f\u007f]/g, " ").trim().slice(0, 80);
        // Adding is what somebody means by putting a folder in a box, so it is
        // the default. Replacing is the much larger claim that this server now
        // serves that instead, so it is the one you have to ask for.
        replacing = body.replace === true;
      } catch {
        json(response, 400, { error: "bad JSON" });
        return;
      }
      if (!source) {
        json(response, 400, { error: "no source given" });
        return;
      }
      try {
        let tracks: Loaded[] = await options.load(source);
        if (tracks.length === 0) {
          json(response, 422, { error: `nothing to play at ${source}` });
          return;
        }
        // A handful of addresses whose names say nothing get asked what they
        // are, so a live channel arrives as a picture rather than as its own
        // soundtrack. Capped, because a playlist of five thousand of them is
        // five thousand ffprobes and the answer only matters for the few a
        // person adds by hand.
        const looked = await Promise.all(
          tracks.map(async (track, at) => {
            if (at >= PROBE_BY_HAND || !nameSaysNothing(track.path)) return track;
            const codecs = await codecsOf(
              { ffmpeg: [], ffprobe: options.ffprobe ?? ["ffprobe"], play: null },
              track.path,
            );
            return codecs.video === "" ? track : { ...track, picture: true };
          }),
        );
        tracks = looked;

        // Named by hand, so a single channel says what it is rather than what
        // its URL ends in.
        if (called !== "" && tracks.length === 1 && tracks[0]) {
          tracks = [{ ...tracks[0], title: called, named: true }];
        }

        let added = tracks.length;
        if (replacing) {
          engine.replace(tracks, source);
        } else {
          added = engine.add(tracks, source, called);
          if (added === 0) {
            // Everything there was already here. Not an error -- the playlist
            // is exactly what the caller asked for -- but worth saying, so a
            // client can tell that apart from having added an album.
            json(response, 200, { ...engine.snapshot(), added: 0, groups: engine.groups() });
            return;
          }
        }
        // Names now, tags later, here as much as at startup: loading a
        // directory of five thousand files used to read every tag before it
        // answered, with the event loop held the whole time.
        if (options.tag) {
          void options
            .tag(source)
            .then((tagged) => engine.retag(tagged, source))
            .catch(() => {});
        }
        json(response, 200, { ...engine.snapshot(), added, replaced: replacing, groups: engine.groups() });
      } catch (error) {
        json(response, 422, { error: (error as Error).message.replace(/^nixamp: /, "") });
      }
      return;
    }

    // A library file in the relay envelope: the same bytes as /api/media/N,
    // framed and compressed once and kept. The original stays where it is
    // and keeps answering ranges; this cannot be asked for one.
    const staticRelay = /^\/api\/media\/(\d+)\/relay$/.exec(path);
    if (staticRelay && request.method === "GET") {
      const compression = compressionCtx();
      if (!compression) {
        json(response, 503, { error: "this server keeps no relay representations" });
        return;
      }
      if (!options.media) {
        json(response, 403, { error: "media streaming is off" });
        return;
      }
      const index = Number(staticRelay[1]);
      const file = engine.trackPath(index);
      if (file === undefined) {
        json(response, 404, { error: "no such track" });
        return;
      }
      handleStaticRelay(request, response, compression, file, engine.snapshot().tracks?.[index]?.title ?? file);
      return;
    }

    // Which library item a playlist track is, for publishing it on demand (guide-vods.ts).
    const mediaItem = /^\/api\/media\/(\d+)\/item$/.exec(path);
    if (mediaItem) {
      const file = engine.trackPath(Number(mediaItem[1]));
      const item = file === undefined ? undefined : options.library?.index.itemAt(file);
      if (!item) {
        json(response, 404, { error: "that track is not in this server's library", code: "not_in_library" });
        return;
      }
      json(response, 200, { id: item.id, video: item.video, title: item.override.title || item.title, duration: item.duration });
      return;
    }

    if (path.startsWith("/api/media/")) {
      if (!options.media) {
        json(response, 403, { error: "media streaming is off" });
        return;
      }
      const index = Number(path.slice("/api/media/".length));
      const file = Number.isInteger(index) ? engine.trackPath(index) : undefined;
      if (file === undefined) {
        json(response, 404, { error: "no such track" });
        return;
      }
      watch(request, response, "media", engine.snapshot().tracks?.[index]?.title ?? file);
      await playFile(request, response, file, url, options, () => engine.sawPicture(index));
      return;
    }

    // Whatever the source is, this comes back as MP3 a browser will play:
    // a flac, a wma, a URL, an HLS stream. ffmpeg reads them all and we hand
    // the bytes on as they arrive, so a live stream starts immediately rather
    // than after it ends, which for a live stream is never.
    // One address that keeps playing, for a listener that cannot ask for the
    // next track: the phone line hands exactly this to Telnyx.
    if (path === "/api/live") {
      if (!options.media) {
        json(response, 403, { error: "media streaming is off" });
        return;
      }
      const current = engine.snapshot();
      if (current.trackCount === 0) {
        json(response, 404, { error: "nothing is playing" });
        return;
      }
      watch(request, response, "stream", current.tracks?.[current.index]?.title ?? "live");
      await liveAudio(
        request,
        response,
        engine,
        options.ffmpeg ?? ["ffmpeg"],
        options.ffprobe ?? ["ffprobe"],
      );
      return;
    }

    if (path.startsWith("/api/stream/")) {
      const index = Number(path.slice("/api/stream/".length));
      const source = Number.isInteger(index) ? engine.trackPath(index) : undefined;
      if (source === undefined) {
        json(response, 404, { error: "no such track" });
        return;
      }
      if (!options.media) {
        json(response, 403, { error: "media streaming is off" });
        return;
      }
      watch(request, response, "stream", engine.snapshot().tracks?.[index]?.title ?? source);
      transcode(request, response, source, options.ffmpeg ?? ["ffmpeg"]);
      return;
    }

    if (path.startsWith("/api/")) {
      json(response, 404, { error: "no such endpoint" });
      return;
    }

    if (web !== null) {
      const direct = safeJoin(web, path);
      if (direct === null) {
        json(response, 400, { error: "bad path" });
        return;
      }
      let file: string | null = null;
      // The library and radio console is a page of its own, at its own names.
      if (/^\/(radio|library)\/?$/.test(path) && isFile(join(web, "radio.html"))) file = join(web, "radio.html");
      else if (isFile(direct)) file = direct;
      else if (isFile(join(direct, "index.html"))) file = join(direct, "index.html");
      // A single-page app: any unknown path is the shell, and the client routes.
      else if (isFile(join(web, "index.html"))) file = join(web, "index.html");
      if (file !== null) {
        if (file.endsWith("index.html") && path.startsWith("/live/") && options.events) {
          try {
            const slug = decodeURIComponent(path.slice("/live/".length).replace(/\/$/, ""));
            const event = await options.events.get(slug);
            const account = await options.accounts?.whoIs(tokenFrom(request.headers)) ?? null;
            const invited = event
              ? await options.events.invitationAllows(event.id, url.searchParams.get("invite") ?? "")
              : false;
            if (event && (await options.events.canAccess(event, account?.id) || invited)) {
              const shell = readIfPossible(file);
              if (shell !== null) {
                html(response, 200, eventDocument(shell, event, eventSite ?? "https://backtoschool.help"));
                return;
              }
            }
          } catch {}
        }
        if (file.endsWith("index.html") && path === "/") {
          // The directory first: a listed server is named by the listing this
          // site already holds, with no request to anybody.
          let subject: JoinSubject | null = joinSubject(url, options);
          if (subject === null) {
            // Not listed. Ask the server the link points at what it is called,
            // rather than reading a name out of the query — a name from the
            // query would let whoever wrote the link choose how the card reads
            // on this domain, which is a phishing primitive, not a feature.
            const link = url.searchParams.get("url") ?? "";
            const play = url.searchParams.get("play") ?? "";
            if (link !== "") {
              subject = await remoteSubject(
                link,
                play.startsWith("channel:") ? play.slice("channel:".length) : "",
              );
            }
          }
          const shell = subject ? readIfPossible(file) : null;
          if (subject && shell !== null) {
            html(response, 200, requestHost === "nixamp.com" ? withPitFooter(joinDocument(shell, subject, eventSite ?? "https://nixamp.com")) : joinDocument(shell, subject, eventSite ?? "https://nixamp.com"));
            return;
          }
        }
        // nixamp.com's own pages (the app shell, the radio console) carry the
        // pit.world global footer, appended on the way out. A local install
        // serving the same files on its own host is left alone.
        if (requestHost === "nixamp.com" && file.endsWith(".html") && (request.method === "GET" || request.method === "HEAD")) {
          const shell = readIfPossible(file);
          if (shell !== null) {
            const page = withPitFooter(shell);
            response.writeHead(200, {
              ...CORS,
              "content-type": "text/html; charset=utf-8",
              "content-length": Buffer.byteLength(page),
              "cache-control": "no-cache",
            });
            response.end(request.method === "HEAD" ? undefined : page);
            return;
          }
        }
        sendFile(request, response, file);
        return;
      }
    }

    json(response, 404, { error: "not found" });
  };
}

/** Read a file, or null. The firewall probe asks about files it may not have. */
/**
 * The public address /radio links are written for: the guide's (watchBase,
 * https://live.streaming.pit.world on server1), else this server's own.
 */
function radioBaseFor(request: IncomingMessage, publicBase: string): string {
  const host = String(request.headers["x-forwarded-host"] ?? request.headers.host ?? "localhost").split(",")[0]!.trim();
  return watchBase(publicBase || `https://${host}`);
}

function readIfPossible(path: string): string | null {
  try {
    return readFileSync(path, "utf8");
  } catch {
    return null;
  }
}

/** How long to wait before looking again when the player has not moved on. */
const LIVE_GAP_MS = 500;
/** How long to wait when there is nothing to play at all yet. */
const LIVE_IDLE_MS = 2000;

/**
 * Whatever is playing, as one endless MP3.
 *
 * /api/stream/N is one track: it needs an index, and it stops at the end of
 * the song. That is right for a browser, which knows what is playing and can
 * ask for the next one. It is wrong for everything that cannot -- a telephone
 * call, `curl | mpv`, anything handed a single address and expected to keep
 * hearing sound. Those need one URL that never ends and never needs asking
 * again, which is what a listener means by "the stream".
 *
 * So this follows the player rather than an index: transcode what is playing,
 * and when that track ends look at what is playing now and keep writing into
 * the same response. The listener sees one continuous audio/mpeg body.
 *
 * Read at native rate (-re), unlike /api/stream/N which is free to run ahead
 * into a browser's buffer. Here running ahead would finish the song in two
 * seconds and then sit waiting for the player to catch up, so the thing that
 * decides what plays next would be minutes behind what the listener hears.
 */
/**
 * The server's own output, as one address that keeps playing.
 *
 * This is the watch party: everybody pointed at it hears and sees whatever the
 * server is playing, and somebody joining halfway through joins halfway
 * through rather than starting the film again on their own.
 *
 * A film comes with its picture. It used to be `-vn` and MP3 whatever it was,
 * so inviting people to watch a film got them its soundtrack -- which is not
 * an invitation anybody wants. The container is decided when the connection
 * opens, because a response has one content type and MP4 and MP3 cannot be
 * spliced; going from a film to a song ends the stream, and a client that
 * wants to keep listening asks again and gets the right one.
 */
async function liveAudio(
  request: IncomingMessage,
  response: ServerResponse,
  engine: Engine,
  ffmpeg: string[],
  ffprobe: string[],
): Promise<void> {
  const [command, ...prefix] = ffmpeg as [string, ...string[]];

  /** Whether this track is something to watch rather than only to hear. */
  const looksLikeVideo = async (source: string): Promise<boolean> => {
    if (hasPicture(source)) return true;
    if (!nameSaysNothing(source)) return false;
    const codecs = await codecsOf({ ffmpeg: [], ffprobe, play: null }, source);
    return codecs.video !== "";
  };

  const first = engine.trackPath(engine.snapshot().index);
  const asVideo = first === undefined ? false : await looksLikeVideo(first);
  let child: ReturnType<typeof spawn> | null = null;
  let waiting: ReturnType<typeof setTimeout> | null = null;
  let closed = false;
  let started = false;
  let playing = -1;

  // Held back until the first byte, for the reason transcode() holds it back:
  // a 200 with nothing behind it is indistinguishable from silence.
  const begin = (): void => {
    if (started || closed) return;
    started = true;
    response.writeHead(200, {
      ...CORS,
      "content-type": asVideo ? "video/mp4" : "audio/mpeg",
      "cache-control": "no-store",
      "transfer-encoding": "chunked",
    });
  };

  const later = (ms: number, run: () => void): void => {
    if (waiting) clearTimeout(waiting);
    waiting = setTimeout(run, ms);
    waiting.unref?.();
  };

  const stop = (): void => {
    if (closed) return;
    closed = true;
    if (waiting) clearTimeout(waiting);
    waiting = null;
    unsubscribe();
    child?.kill("SIGKILL");
    child = null;
    if (!response.writableEnded) response.end();
  };

  const next = (): void => {
    if (closed || child !== null) return;
    if (waiting) {
      clearTimeout(waiting);
      waiting = null;
    }
    const snapshot = engine.snapshot();
    const source = engine.trackPath(snapshot.index);
    if (source === undefined) {
      // A playlist that was replaced out from under us, or one that is empty
      // for the moment. Keep the connection and keep looking.
      later(LIVE_IDLE_MS, next);
      return;
    }

    playing = snapshot.index;
    // Joined where the server is, not where the track begins. Somebody
    // arriving forty minutes into a film should arrive forty minutes in;
    // starting it again for them is not a watch party, it is two people
    // watching the same film separately.
    //
    // Before -i, so ffmpeg seeks rather than decoding its way there.
    const from = Math.max(0, Math.floor(snapshot.position));
    const spawned = spawn(
      command,
      [
        ...prefix,
        "-hide_banner",
        "-loglevel", "error",
        ...(isRemote(source) ? ["-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "5"] : []),
        // A live source has no beginning to seek from.
        ...(from > 1 && !isRemote(source) ? ["-ss", String(from)] : []),
        "-re",
        ...protocolArgs(source),
        "-i", source,
        ...(asVideo
          // Copied where it can be, because a room full of viewers is a room
          // full of encoders otherwise.
          ? ["-c:v", "copy", "-c:a", "aac", "-b:a", "160k", "-ac", "2",
             "-f", "mp4", "-movflags", "frag_keyframe+empty_moov+default_base_moof"]
          : ["-vn", "-f", "mp3", "-b:a", "192k"]),
        "-",
      ],
      { stdio: ["ignore", "pipe", "pipe"] },
    );
    child = spawned;

    spawned.stdout.once("data", begin);
    spawned.stdout.on("error", () => spawned.kill("SIGKILL"));
    // end: false, because the response outlives this track. Ending it here is
    // exactly the bug this endpoint exists to avoid.
    spawned.stdout.pipe(response, { end: false });
    spawned.stderr.resume();

    spawned.on("error", stop);
    spawned.on("close", () => {
      if (child !== spawned) return;
      child = null;
      if (closed) return;
      const now = engine.snapshot();
      // Follow the player if it has already moved on.
      if (now.index !== playing) {
        later(0, next);
        return;
      }
      // It has not. Served like this the engine decodes nothing itself, so
      // nothing ever ends a track on its side and the index never moves on
      // its own: a podcast playlist streamed to a room played its first
      // episode to the end and then played it again, for ever. This stream
      // is the playback, so it is the one that moves the player on. One
      // track alone still repeats rather than falls silent, as before.
      if (now.trackCount > 1) engine.command({ type: "next" });
      later(LIVE_GAP_MS, next);
    });
  };

  // A track change that lands while we are between songs is the signal to go
  // now rather than wait out the poll.
  const unsubscribe = engine.subscribe(() => {
    if (closed || child !== null) return;
    if (engine.snapshot().index === playing) return;
    // The kind changed under us -- a film after a song, or the other way --
    // and one response cannot carry both. Ending it is how the client is told
    // to ask again, which it does.
    const source = engine.trackPath(engine.snapshot().index);
    if (source !== undefined) {
      void looksLikeVideo(source).then((wants) => {
        if (closed) return;
        if (wants !== asVideo) stop();
        else next();
      });
      return;
    }
    next();
  });

  response.on("close", stop);
  response.on("error", stop);
  request.on("close", stop);
  next();
}

/**
 * Decode anything and hand back MP3, as it is produced.
 *
 * No seeking: this is a pipe, and the length is not known until it ends. The
 * player falls back to /api/media for a local file it can seek, and uses this
 * for everything else.
 */
/** Audio, from whatever this is: the shape every non-browser source took. */
/**
 * A file as a browser plays it: the bytes themselves, seekable, when the
 * browser can; otherwise an MP4 or MP3 made on the way. `onPicture` hears
 * that ffprobe found a picture in it.
 */
async function playFile(
  request: IncomingMessage, response: ServerResponse, file: string, url: URL,
  options: Pick<HandlerOptions, "ffmpeg" | "ffprobe">, onPicture: () => void = () => undefined,
): Promise<void> {
  // A browser asks for every track here, and a matroska or an avi handed
  // to it raw is bytes it cannot play. Seeking is what this route is for
  // and transcoding gives it up, but an unseekable film beats a silent
  // one -- and the seekable formats are untouched.
  // A ceiling the caller asked for, because only the caller knows what its
  // link can carry. Capped at both ends: nothing below 200k is watchable,
  // and above 20 megabits the original was always the better answer.
  const asked = Number(url.searchParams.get("kbps") ?? "");
  const capKbps = Number.isFinite(asked) && asked > 0 ? Math.min(20_000, Math.max(200, asked)) : 0;
  // Whether this browser decodes H.265, which only it can know: Safari and
  // televisions do, Chrome on a desktop does not and says nothing when
  // handed it. Copying a 4K HEVC film is free; re-encoding one does not
  // keep up with playing it, so the answer is worth carrying in the URL.
  const allowHevc = url.searchParams.get("hevc") === "1";

  if (playsInBrowser(file) && capKbps === 0) {
    sendFile(request, response, file);
    return;
  }
  // A film, or something whose name refuses to say. A live channel at
  // .../301 used to fall through to the audio branch and arrive as MP3
  // with `-vn` -- a match you could only listen to.
  if (hasPicture(file) || nameSaysNothing(file)) {
    // What ffprobe finds inside decides how little work it takes to keep
    // the picture, and whether there is a picture to keep at all.
    const codecs = await codecsOf({ ffmpeg: [], ffprobe: options.ffprobe ?? ["ffprobe"], play: null }, file);
    if (codecs.video !== "") {
      // Told back to the playlist, so an entry that was added before this
      // could be asked stops claiming to be a song. Without it a track
      // added by an older nixamp goes to an audio element for ever, and
      // the only cure is noticing and adding it again.
      onPicture();
      pipeFfmpeg(
        request, response, file, options.ffmpeg ?? ["ffmpeg"],
        videoArgs(codecs, capKbps, { allowHevc }), "video/mp4",
        transportInputArgs(file, codecs.container),
      );
      return;
    }
  }
  transcode(request, response, file, options.ffmpeg ?? ["ffmpeg"]);
}

function transcode(
  request: IncomingMessage,
  response: ServerResponse,
  source: string,
  ffmpeg: string[],
): void {
  pipeFfmpeg(request, response, source, ffmpeg, ["-vn", "-f", "mp3", "-b:a", "192k"], "audio/mpeg");
}

/**
 * Run ffmpeg and hand its output straight to the caller.
 *
 * The output arguments belong to the caller, because the same plumbing carries
 * a film and a song and the only difference is what ffmpeg is asked to write --
 * which for a film is decided by what ffprobe found inside it.
 */
function pipeFfmpeg(
  request: IncomingMessage,
  response: ServerResponse,
  source: string,
  ffmpeg: string[],
  outputArgs: string[],
  contentType: string,
  /**
   * What to say before the input is opened. A transport stream needs telling
   * how far to read before it decides what is in it, and to make up the
   * timestamps a recording cut mid-stream does not have.
   */
  inputArgs: string[] = [],
): void {
  const [command, ...prefix] = ffmpeg as [string, ...string[]];
  const child = spawn(
    command,
    [
      ...prefix,
      "-hide_banner",
      "-loglevel", "error",
      // Reconnect through the sort of hiccup a long stream runs into. These
      // belong to the http protocol, and ffmpeg rejects the whole command
      // when they are handed to it for a file on disk.
      ...(isRemote(source) ? ["-reconnect", "1", "-reconnect_streamed", "1", "-reconnect_delay_max", "5"] : []),
      ...inputArgs,
      ...protocolArgs(source),
      "-i", source,
      ...outputArgs,
      "-",
    ],
    { stdio: ["ignore", "pipe", "pipe"] },
  );

  let failed = "";
  child.stderr.on("data", (chunk: Buffer) => {
    // Keep the tail: ffmpeg says what went wrong on its last line.
    failed = (failed + chunk.toString()).slice(-2000);
  });

  let started = false;
  const begin = (): void => {
    if (started) return;
    started = true;
    response.writeHead(200, {
      ...CORS,
      "content-type": contentType,
      "cache-control": "no-store",
      // Length is unknowable up front, and a browser is happy without it.
      "transfer-encoding": "chunked",
    });
  };
  // Wait for a first byte before promising success. ffmpeg rejects a bad option
  // or a missing input immediately, and answering 200 with nothing looks the
  // same from a player as a track that is simply silent.
  child.stdout.once("data", begin);
  // Both ends can fail: a listener closing the tab breaks the socket under the
  // pipe, and an EPIPE nobody is listening for takes the process down.
  child.stdout.on("error", () => child.kill("SIGKILL"));
  response.on("error", () => child.kill("SIGKILL"));
  // `end: false`, because a pipe that closes the response also commits its
  // headers -- and ffmpeg failing instantly ends stdout without ever writing a
  // byte. The response would go out as an empty 200, and the close handler
  // below would then try to send a 502 over it and throw
  // ERR_HTTP_HEADERS_SENT from a child-process callback, which is not a place
  // an exception can be caught: it killed the whole server. A URL that ffmpeg
  // cannot read is an ordinary thing for a person to paste, and it took every
  // listener down with it.
  child.stdout.pipe(response, { end: false });

  child.on("error", (error) => {
    console.error(`nixamp: ffmpeg could not start: ${error.message}`);
    if (!response.headersSent) json(response, 500, { error: "ffmpeg could not start" });
    else if (!response.writableEnded) response.end();
  });
  child.on("close", (code) => {
    const message = failed.trim();
    if (code !== 0 && code !== null) console.error(`nixamp: ffmpeg exited ${code}: ${message}`);
    // Asked of the response rather than of our own flag: the truth about
    // whether a status can still be sent belongs to the response.
    if (!started && !response.headersSent) {
      // Nothing was ever produced, so the status can still tell the truth.
      json(response, 502, { error: "could not decode that source", detail: message.split("\n").pop() ?? "" });
      return;
    }
    if (!response.writableEnded) response.end();
  });

  // A listener that closes the tab should not leave an ffmpeg decoding into
  // nothing for the rest of the album.
  const stop = (): void => {
    child.kill("SIGKILL");
  };
  request.on("close", stop);
  response.on("close", stop);
}

function isFile(path: string): boolean {
  try {
    return statSync(path).isFile();
  } catch {
    return false;
  }
}

function sendFile(request: IncomingMessage, response: ServerResponse, file: string, cache?: string): void {
  let size: number;
  try {
    size = statSync(file).size;
  } catch {
    json(response, 404, { error: "not found" });
    return;
  }
  const type = contentType(file);
  const range = parseRange(request.headers.range, size);
  const headers: Record<string, string> = {
    ...CORS,
    "content-type": type,
    "accept-ranges": "bytes",
  };
  // The shell must never be cached by a service worker's fetch fallback, but
  // hashed assets and audio can be.
  headers["cache-control"] = cache ?? (type.startsWith("text/html") ? "no-cache" : "public, max-age=3600");

  if (range) {
    headers["content-range"] = `bytes ${range.start}-${range.end}/${size}`;
    headers["content-length"] = String(range.end - range.start + 1);
    response.writeHead(206, headers);
  } else {
    headers["content-length"] = String(size);
    response.writeHead(200, headers);
  }
  if (request.method === "HEAD") {
    response.end();
    return;
  }
  const stream = range
    ? createReadStream(file, { start: range.start, end: range.end })
    : createReadStream(file);
  stream.on("error", () => response.destroy());
  response.on("close", () => stream.destroy());
  stream.pipe(response);
}

export function createServer(engine: Engine, options: HandlerOptions): Server {
  const handle = createHandler(engine, options);
  const onRequest = (request: IncomingMessage, response: ServerResponse): void => {
    handle(request, response).catch((error: unknown) => {
      // Silent by default: a stack trace per bad request is a log nobody
      // reads. Said aloud when asked, which is how a 500 in a test is found.
      if (process.env["NIXAMP_DEBUG"]) console.error(error);
      if (!response.headersSent) json(response, 500, { error: "server error" });
      else response.end();
    });
  };

  // https when there is a certificate to serve it with, and the same handler
  // either way: nothing above this line knows or cares which it got.
  if (options.tls) {
    return createHttpsServer({ cert: options.tls.cert, key: options.tls.key }, onRequest) as unknown as Server;
  }
  return createHttpServer(onRequest);
}


export async function serve(argv: string[], version = "0.1.0"): Promise<void> {
  const options = parseServeArgs(argv);
  // Told which folder, or the one that was saved. Never the directory this
  // happens to be running in: a daemon restarted from a home directory served
  // the home directory, keys and all, under a public listing.
  const chosen = options.root || readLibrary();
  if (!chosen) {
    throw new Error(
      "nixamp serve: which folder? Say `nixamp library ~/Music` once, or `nixamp daemon start ~/Music`.",
    );
  }
  const root = isRemote(chosen) ? chosen : resolve(chosen);
  // What the stream is called where people see it: the name given, else the
  // folder or playlist it was started on, in words, else this machine.
  const streamName = options.name || humanizeSource(root) || hostname();
  const why = isRemote(root) ? "" : forbiddenLibrary(root);
  if (why) {
    throw new Error(`nixamp will not serve ${why}. Pick a folder with your media in it: nixamp library ~/Music`);
  }
  const tools = detectTools();
  // Names now, tags later.
  //
  // Reading tags is an ffprobe per file, which over a real library is minutes,
  // and every one of them used to happen before this process printed a word or
  // listened on a port. `nixamp serve ~/music` looked hung, and `nixamp daemon
  // start` was worse: it waits fifteen seconds for the announce line, killed a
  // daemon that was working perfectly, and reported a failure whose log was
  // empty because nothing had been written to it yet.
  //
  // So the filenames are enough to start: the server is up and answering in the
  // time it takes to walk the directory, and the titles fill in behind it.
  // Empty on purpose. The walk happens below, once the port is open: it is
  // the slowest part of starting and nothing about it needs to happen first.
  const engine = new PlayerEngine([], root, tools);

  // A serving machine with speakers is still a player. A headless one has no
  // ffplay, and playJingle answers that by doing nothing.
  if (!options.noJingle) playJingle(tools);

  const web = options.web !== null ? resolve(options.web) : defaultWebDir();
  const { webSitesFromEnv } = await import("./web-sites.ts");
  const webSites = webSitesFromEnv();
  // The same keys this port used last time, so a link somebody was given
  // still works after a restart -- and a server is restarted to pick up a new
  // version, which is to say often. `--new-key` mints a fresh pair and forgets
  // the old one, which is the way to revoke a link that got out.
  // Whether this port had keys before this start: a server that has run
  // here before, whose directory listing (see publishChoice) is kept.
  const established = existsSync(join(stateDir(), "keys.json")) &&
    (() => { try { return String(options.port) in (JSON.parse(readFileSync(join(stateDir(), "keys.json"), "utf8")) as object); } catch { return false; } })();
  const remembered = options.key ? rememberedKeys(stateDir(), options.port, options.newKey) : null;
  const key = remembered?.key ?? null;
  // Kept whether or not it is published, so `nixamp admin` and the operator
  // both have a link they can hand out without handing over the controls.
  const listenKey = remembered?.listenKey ?? null;
  // Configuration can arrive from the directory later, so it is a box the
  // paywall reads rather than a value it was handed once.
  let paywallConfig: PaywallConfig = { ...paywallFromEnv(), enabled: options.x402 || paywallFromEnv().enabled };
  const connections = new Connections();
  const paywall = createPaywall({
    config: () => paywallConfig,
    liveListeners: () => connections.listening,
    // The address a payer can actually reach: the public one where there is
    // one, since a quote pointing at 192.168.1.5 is one they cannot pay from.
    siteUrl: () => {
      const bound = server.address();
      const live = typeof bound === "object" && bound !== null ? bound.port : options.port;
      const reachable = reachableAddresses(options.host, live);
      return (reachable.find((a) => a.label === "on the internet") ?? reachable[0])?.url
        ?? `http://127.0.0.1:${live}`;
    },
    // The operator drives with the control key, and is not a customer.
    exempt: (request) =>
      key !== null && scopeOf(keyFrom(request, new URL(request.url ?? "/", "http://localhost")), key, null) === "control",
  });

  // The pictures of channels, taken once each and kept while the channel is on.
  const art = new ArtCache();
  // What a channel plays once its show is over: drawn here, once, by the
  // same ffmpeg that carries the channels. No ffmpeg, no outro, and a show
  // that ends closes its channel as before.
  const outro = new Outro({ ffmpeg: tools.ffmpeg, dir: join(stateDir(), "outro"), onEvent: (message) => console.log(message) });
  // Made once the channels exist; told of every channel that ends, so a
  // station that died is brought back and one that was stopped is not.
  let stationRunner: StationRunner | null = null;
  const channels = new Channels({
    ffmpeg: tools.ffmpeg,
    ffprobe: tools.ffprobe,
    ...(tools.carries ? { outro: (kind) => outro.clip(kind) } : {}),
    onOutro: (info) => console.log(`  "${info.id}" has ended; the outro plays for an hour.`),
    onStart: (info) =>
      console.log(`  ${info.name} is publishing to "${info.id}" (${info.format} over ${info.via}).`),
    onEnd: (info) => {
      console.log(`  "${info.id}" stopped.`);
      art.forget(info.id);
      stationRunner?.onChannelEnd(info);
    },
  });
  // The library as an index kept beside the keys: what each file is, learned
  // once and asked about again only when it changes. The folder this server
  // was started on is its first source.
  const libraryIndex = new LibraryIndex(LibraryIndex.fileFor(stateDir(), options.port));
  const homeSource = (() => {
    try {
      return !isRemote(root) && statSync(root).isDirectory() ? libraryIndex.ensureSource(root, options.owner || "") : null;
    } catch {
      return null;
    }
  })();
  const probeFile = (path: string): Promise<Probed> => probeItem(tools, path);
  const retagPlayer = (tracks: Track[]): void => {
    if (tracks.length > 0) engine.retag(tracks, root);
  };
  const listeners = new ListenerStore(ListenerStore.fileFor(stateDir(), options.port));
  const library = { index: libraryIndex, probe: probeFile, retag: retagPlayer, listeners };
  const stationStore = new StationStore(StationStore.fileFor(stateDir(), options.port));
  // What stations deliver, per month, and the operator's limits on it.
  const stationUsage = new StationUsage(StationUsage.fileFor(stateDir(), options.port));
  const stationLimits = limitsFromEnv(process.env, readConfig().limits ?? {});
  process.once("exit", () => stationUsage.flush());
  // Breaks between songs and films: the network's spot, or a house one.
  const stationBreaks = new StationBreaks({
    dir: join(stateDir(), "ads"),
    site: process.env["NIXAMP_SITE"] || "https://nixamp.com",
    hasSlot: Boolean(process.env["AD_SLOT"]),
    guard: (url) => refusedDestination(url),
  });
  stationRunner = new StationRunner({
    breaks: (video) => stationBreaks.next(video ? "video" : "audio"),
    onRestart: (id) => stationUsage.restarted(id),
    onFailure: (id) => stationUsage.failed(id),
    artistOf: (file) => trackFor(engine, file)?.artist ?? "",
    // Nobody listening for this long and a station sleeps until somebody does.
    idleSeconds: () => stationIdleSeconds(process.env, readConfig()),
    store: stationStore,
    channels,
    pull: async (channelId, name, files, known) =>
      Boolean(await pullChannel(channels, tools.ffprobe, channelId, name, files[0]!, [], "", {
        playlist: files,
        live: true,
        rotation: known.rotation,
        audioBitrate: known.audioBitrate,
      })),
    // The library as it is now, or as it was chosen while it is still being read.
    roots: () => [...new Set([engine.snapshot(false).root || chosen, ...libraryIndex.roots()])].filter((one) => one !== ""),
    log: (line) => console.log(line),
  });
  // Auto stations: a station per genre, kept in step with the library.
  const autoSetting = (): AutoOptions | null => {
    const saved = readConfig().autoStations;
    if (!saved) return null;
    const pick = (name: keyof AutoOptions) => (saved[name] === undefined ? DEFAULT_AUTO[name] : saved[name]);
    return {
      video: pick("video") !== false,
      minItems: Number(pick("minItems")) || DEFAULT_AUTO.minItems,
      minFilms: Number(pick("minFilms")) || DEFAULT_AUTO.minFilms,
      start: pick("start") !== false,
      maxVideoRunning: Number(pick("maxVideoRunning")) >= 0 ? Number(pick("maxVideoRunning")) : DEFAULT_AUTO.maxVideoRunning,
      breaksEvery: Number(pick("breaksEvery")) >= 0 ? Number(pick("breaksEvery")) : DEFAULT_AUTO.breaksEvery,
    };
  };
  let autoRunning: Promise<unknown> | null = null;
  let autoLast: { at: number; report: unknown } | null = null;
  const runAuto = (settings: AutoOptions): Promise<unknown> => {
    if (autoRunning) return autoRunning;
    autoRunning = syncAutoStations({
      index: libraryIndex,
      store: stationStore,
      runner: stationRunner!,
      owner: options.owner || "",
      mayStart: () => stationLimits.runningStations === 0 || stationStore.list().filter((one) => one.desired === "running").length < stationLimits.runningStations,
      // Only what is already known: a sync over a whole library must not ask
      // nichedb a question per file (it queued minutes of queries there).
      filmGenres: async (title) => {
        const hit = enricher.known(title);
        const genres = hit?.data?.["genres"];
        return Array.isArray(genres) ? genres.filter((one): one is string => typeof one === "string") : [];
      },
      log: (line) => console.log(line),
    }, settings).then((report) => {
      const made = report as { radio: unknown[]; tv: unknown[] };
      console.log(`  auto stations: ${made.radio.length} radio, ${made.tv.length} TV.`);
      autoLast = { at: Date.now(), report };
      return report;
    }).finally(() => { autoRunning = null; });
    return autoRunning;
  };
  libraryIndex.onScanned = () => {
    const settings = autoSetting();
    if (settings) void runAuto(settings);
  };
  const stationAuto = {
    setting: () => ({ ...(autoSetting() ?? { off: true }), running: autoRunning !== null, last: autoLast }),
    sync: async (input: Record<string, unknown>) => {
      if (input["off"] === true) {
        writeConfig({ autoStations: undefined });
        return { off: true };
      }
      const settings: AutoOptions = { ...DEFAULT_AUTO, ...(autoSetting() ?? {}) };
      for (const name of Object.keys(DEFAULT_AUTO) as (keyof AutoOptions)[]) {
        if (input[name] !== undefined) (settings as unknown as Record<string, unknown>)[name] = input[name];
      }
      writeConfig({ autoStations: settings as unknown as Record<string, unknown> });
      // In the background: a big library takes a while, and an HTTP request
      // that waits for it times out before it is done.
      void runAuto(settings);
      return { started: true, settings };
    },
  };
  const stations: NonNullable<HandlerOptions["stations"]> = {
    store: stationStore, runner: stationRunner, usage: stationUsage, limits: stationLimits, auto: stationAuto,
    trustedProxies: () => new Set([...(process.env["NIXAMP_TRUSTED_PROXIES"] ?? "").split(","), ...(readConfig().trustedProxies ?? [])].map((one) => one.trim()).filter(Boolean)),
    // Known once the server has worked out its addresses, after it is listening.
    publicBase: () => (publishable_?.url ?? "").replace(/\/+$/, ""),
    // Other servers' guides: the configured peers, and on a directory
    // (nixamp.com) every server listed in it, so its guide is every public
    // station anywhere.
    peers: new PeerGuides(() => {
      const listed = directory ? directory.list().map((one) => peerBase(one.url)).filter(Boolean).slice(0, 100) : [];
      return [...new Set([...guidePeers(), ...listed])];
    }),
    // Live rooms published to this guide; the probe never reaches this machine's own network.
    rooms: { store: new RoomStore(RoomStore.fileFor(stateDir(), options.port)), probe: new RoomProbe(fetch, (link) => refusedDestination(link)) },
    // Files published to play on demand; the same guard on whose network the probe reaches.
    vods: { store: new VodStore(VodStore.fileFor(stateDir(), options.port)), probe: new VodProbe(fetch, (link) => refusedDestination(link)) },
  };
  // Music handed over from BitTorrented, copied into a folder of our own.
  const importer = new Importer(join(stateDir(), `handoffs-${options.port}.json`), {
    index: libraryIndex,
    stations: stationStore,
    root: join(stateDir(), "imports", "bittorrented"),
    probe: probeFile,
    guard: (url) => refusedDestination(url),
    log: (line) => console.log(line),
  });
  importer.resume();
  const mayStartStation = (): boolean =>
    stationLimits.runningStations === 0 || stationStore.list().filter((one) => one.desired === "running").length < stationLimits.runningStations;
  // AI radio: music composed for its stations, a few tracks a day, in a folder of our own.
  const aiRadio = new AiRadio(AiRadio.fileFor(stateDir(), options.port), {
    index: libraryIndex,
    store: stationStore,
    runner: stationRunner,
    probe: probeFile,
    ffmpeg: tools.ffmpeg,
    root: join(stateDir(), "ai-music"),
    owner: options.owner || "",
    holder: () => aiLabel.holder(),
    mayStart: mayStartStation,
    log: (line) => console.log(line),
  });
  // The AI label: artists who put out albums on their own, filed under the
  // library as ai/<genre>/<artist>/<year>-<album>/ (NIXAMP_LABEL_ROOT moves it).
  const labelHome = libraryIndex.sources().find((one) => one.enabled && !one.managed);
  const aiLabel = new AiLabel(AiLabel.fileFor(stateDir(), options.port), {
    index: libraryIndex,
    store: stationStore,
    runner: stationRunner,
    probe: probeFile,
    ffmpeg: tools.ffmpeg,
    root: process.env["NIXAMP_LABEL_ROOT"] || (labelHome ? join(labelHome.root, "ai") : join(stateDir(), "ai-label")),
    owner: options.owner || "",
    mayStart: mayStartStation,
    // Filmed shots for singles come from nixamp.com's Veo studio, asked as this server's owner, under its monthly cap.
    veo: async (shot) => {
      const session = readSession();
      const here = stations.publicBase?.() ?? "";
      if (!session?.token || !/^https:\/\//.test(here)) throw new VeoUnavailable("this server is not signed in to nixamp.com");
      const answer = await fetch(`${session.site.replace(/\/+$/, "")}/api/v1/music/veo`, {
        method: "POST",
        headers: { "content-type": "application/json", authorization: `Bearer ${session.token}`, "x-nixamp-server": here },
        body: JSON.stringify({ prompt: shot.prompt, seconds: shot.seconds, references: shot.references.map((one) => one.toString("base64")) }),
        signal: AbortSignal.timeout(12 * 60_000),
      });
      if (answer.status === 402 || answer.status === 404) throw new VeoUnavailable(((await answer.json().catch(() => ({}))) as { error?: string }).error ?? `HTTP ${answer.status}`);
      if (!answer.ok) throw new Error(((await answer.json().catch(() => ({}))) as { error?: string }).error ?? `nixamp.com answered ${answer.status}`);
      return Buffer.from(await answer.arrayBuffer());
    },
    // A release is told to nixamp.com as this server's owner; nixamp.com tells the artist's followers.
    announce: (release) => {
      const session = readSession();
      const here = stations.publicBase?.() ?? "";
      if (!session?.token || !/^https:\/\//.test(here)) return;
      void fetch(`${session.site.replace(/\/+$/, "")}/api/v1/music/released`, {
        method: "POST",
        headers: { "content-type": "application/json", authorization: `Bearer ${session.token}` },
        body: JSON.stringify({ ...release, server: here }),
        signal: AbortSignal.timeout(15_000),
      }).then((answer) => console.log(`  ai label: nixamp.com was told of "${release.title}" (${answer.status})`)).catch(() => undefined);
    },
    log: (line) => console.log(line),
  });
  aiRadio.startSchedule();
  aiLabel.startSchedule();
  stations.label = aiLabel;
  // Tracks anybody shares from the guide, checked by AI before they air.
  const listenerShares = new ListenerShares(ListenerShares.fileFor(stateDir(), options.port), {
    index: libraryIndex,
    store: stationStore,
    runner: stationRunner,
    probe: probeFile,
    guard: (url) => refusedDestination(url),
    root: join(stateDir(), "shares"),
    owner: options.owner || "",
    mayStart: mayStartStation,
    nowPlaying: (id) => {
      const info = channels.info(channelOf(id));
      return info?.playlist?.[info.playlistAt ?? 0] ?? "";
    },
    log: (line) => console.log(line),
  });
  stations.ai = aiRadio;
  stations.shares = listenerShares;
  // The now-playing card: the file's own tags, where it came from here, and
  // the copyright holders the owner registered.
  const tagReader = new TagReader(tools.ffprobe);
  const holderStore = new HolderStore(HolderStore.fileFor(stateDir(), options.port));
  stations.holders = holderStore;
  // Covers read once per file (and its size and time), a few hundred kept.
  const covers = new Map<string, { key: string; picture: { bytes: Buffer; type: string } | null }>();
  stations.cover = async (playing) => {
    // A label music video's cover is its song's.
    const file = aiLabel.audioFor(playing);
    let key = "";
    try { const info = statSync(file); key = `${info.size}:${info.mtimeMs}`; } catch { return null; }
    const hit = covers.get(file);
    if (hit && hit.key === key) return hit.picture;
    const bytes = await coverArtOf(tools.ffmpeg, file).catch(() => null);
    const picture = bytes && bytes.length > 0 ? { bytes, type: bytes[0] === 0x89 ? "image/png" : "image/jpeg" } : null;
    if (covers.size > 300) covers.clear();
    covers.set(file, { key, picture });
    return picture;
  };
  // The words of what is on: the file's own tag, else nichedb.dev's.
  const lyricsFinder = new LyricsFinder({ tags: (file) => tagReader.read(file), known: (file) => trackFor(engine, file) });
  stations.lyrics = (_station, file) => lyricsFinder.find(aiLabel.audioFor(file));
  // Our own songs' words, back to nichedb, when a key is set: on any station
  // change, after a library scan, and once at start.
  const lyricsPublisher = new LyricsPublisher({
    file: LyricsPublisher.fileFor(stateDir(), options.port),
    key: () => aiKey("NIXAMP_LYRICS_KEY"),
    stations: () => stationStore.list(),
    tags: (file) => tagReader.read(file),
    known: (file) => trackFor(engine, file),
    log: (line) => console.log(line),
  });
  stations.lyricsPublisher = lyricsPublisher;
  stationStore.onSaved = () => lyricsPublisher.soon();
  const scannedBefore = libraryIndex.onScanned;
  libraryIndex.onScanned = (report) => {
    scannedBefore?.(report);
    lyricsPublisher.soon();
  };
  lyricsPublisher.soon();
  stations.facts = async (station, file) => {
    const share = listenerShares.atFile(file);
    const composed = aiRadio.trackAt(file);
    const picture = await stations.cover!(file);
    let stamp = "";
    try { stamp = String(Math.round(statSync(file).mtimeMs)); } catch { stamp = ""; }
    const art = picture ? `/api/v1/stations/${station.id}/art?t=${encodeURIComponent(station.listenToken)}&f=${sha("sha1").update(file + stamp).digest("hex").slice(0, 12)}` : "";
    // A label music video is told about as its song: the MP3 holds the tags and the words.
    const audio = aiLabel.audioFor(file);
    const words = await lyricsFinder.find(audio).catch(() => null);
    // A label song is for sale on nixamp.com: the song, its album, everything by the artist.
    const song = aiLabel.songAt(audio);
    const here = stations.publicBase?.() ?? "";
    const shop = (process.env["NIXAMP_SITE"] || "https://nixamp.com").replace(/\/+$/, "");
    const store = song && /^https:\/\//.test(here) ? {
      song: buyLink(shop, here, "song", song.artist, song.album, song.n),
      album: buyLink(shop, here, "album", song.artist, song.album),
      artist: buyLink(shop, here, "artist", song.artist),
      artistName: song.artistName,
      albumTitle: song.albumTitle,
    } : null;
    return { ...factsFor({
      art,
      ...(words ? { lyrics: `/api/v1/stations/${station.id}/lyrics?t=${encodeURIComponent(station.listenToken)}&f=${sha("sha1").update(file + stamp).digest("hex").slice(0, 12)}` } : {}),
      station, file,
      tags: await tagReader.read(audio),
      known: trackFor(engine, audio),
      holders: holderStore.list(),
      share: share ? { basis: share.rights.basis, note: share.rights.note, by: share.by } : null,
      aiTrack: composed ? { made: composed.made } : null,
    }), store };
  };
  // Channels as HLS, on demand, for Safari on a phone: one ffmpeg copying a
  // channel's fragments into short files while somebody is asking for them.
  // What things are, from nichedb.dev, remembered beside the keys so a
  // library is asked about once across restarts.
  const enricher = new Enricher({
    site: process.env["NIXAMP_NICHEDB"] || NICHEDB,
    cacheFile: join(stateDir(), "enrich.json"),
    onEvent: (message) => console.log(message),
  });
  // Off unless a policy says otherwise: every channel plays exactly as it
  // did, and a relay is a thing another nixamp asks for by name.
  const compression = new CompressionService({
    channels,
    stateDir: stateDir(),
    port: options.port,
    ffprobe: tools.ffprobe,
    cacheDir: join(stateDir(), "relay-cache"),
    onEvent: (message) => console.log(message),
  });
  const hls = new HlsPackagers({
    ffmpeg: tools.ffmpeg,
    listen: (id, listener) => channels.listen(id, listener),
    onEvent: (message) => console.log(message),
    // TS unless the channel's policy, or the server's, asks for fMP4.
    packaging: (id) => compression.packagingOf(id),
  });

  // The channels this server was carrying when it was last stopped, put back
  // on. A server is restarted to pick up a new version, which is often, and
  // every restart used to take CNN off the air until somebody noticed.
  const remembering = (list: RememberedChannel[]): void => rememberChannels(stateDir(), options.port, list);
  for (const one of rememberedChannels(stateDir(), options.port)) {
    const where = one.position && (!one.live || one.playlist?.length) ? `, from ${Math.floor(one.position / 60)}m${Math.floor(one.position % 60)}s` : "";
    console.log(`  Putting "${one.id}" (${one.name}) back on the air${where}.`);
    // With what was written down about it: what it holds, so the source is
    // not asked again, and where it had got to, so a film carries on.
    void pullChannel(channels, tools.ffprobe, one.id, one.name, one.source, [], "", {
      ...(one.kind ? { kind: one.kind } : {}),
      ...(one.codecs ? { codecs: one.codecs } : {}),
      ...(one.position !== undefined ? { position: one.position } : {}),
      ...(one.live !== undefined ? { live: one.live } : {}),
      ...(one.playlist ? { playlist: one.playlist, playlistAt: one.playlistAt ?? 0 } : {}),
      ...(one.art ? { art: one.art } : {}),
      ...(one.about ? { about: one.about } : {}),
    }).then((channel) => {
      if (!channel) console.log(`  "${one.id}" is already on.`);
    });
  }
  stationRunner.restore();
  stationRunner.watchIdle();
  // Where each film has got to, written down every so often, so a restart
  // picks it up from about there rather than from the start. Only when it
  // has changed: a server carrying nothing but live television writes
  // nothing.
  let lastRemembered = "";
  setInterval(() => {
    const now = rememberedNow(channels);
    // Both the entry and its elapsed time advance during a directory live.
    if (!now.some((one) => one.position !== undefined || one.playlist)) return;
    const text = JSON.stringify(now);
    if (text === lastRemembered) return;
    lastRemembered = text;
    remembering(now);
  }, REMEMBER_EVERY_MS).unref();

  // The m3u catalogs kept here: read from disk now, and any that were never
  // read are fetched in the background so browsing does not wait on a provider.
  const catalogs = new Catalogs(stateDir(), options.port);
  catalogs.load();
  catalogs.ensureDefaults();
  void catalogs.warm().catch(() => undefined);

  const destinations = parseDestinations(options.rtmp);
  const broadcaster = new Broadcaster(tools.ffmpeg);
  const ingest = options.ingest
    ? new Ingest({
        ffmpeg: tools.ffmpeg,
        sink: "pipe:1",
        onStart: (session) => console.log(`  ${session.name} started streaming in (${session.format}).`),
        onEnd: (session, error) =>
          console.log(`  ${session.name} stopped streaming in${error ? `: ${error}` : ""}.`),
      })
    : undefined;

  // The account signed in on this machine owns the server it starts. That is
  // the whole claim: `nixamp login` then `nixamp serve`, and the phone in your
  // pocket can administer it from anywhere by signing in as the same person.
  // Following outlives every stream, so unlike the rest of this it wants a
  // database. Only where there is one: a nixamp on a laptop has no followers.
  const databaseUrl = libpqDatabaseUrl(process.env["DATABASE_URL"]);
  const pool =
    options.directory && databaseUrl
      ? new pg.Pool({ connectionString: databaseUrl })
      : undefined;
  const follows = pool ? new Follows(pool) : undefined;
  const favorites = pool ? new Favorites(pool) : undefined;
  const settingsSync = pool ? new SettingsSync(pool) : undefined;
  const nixampSite = (process.env["NIXAMP_SITE"] ?? DEFAULT_DIRECTORY).replace(/\/+$/, "");
  const schoolMailKey = process.env["BACKTOSCHOOL_RESEND_API_KEY"] ?? process.env["RESEND_API_KEY"];
  const schoolMail = schoolMailKey && process.env["BACKTOSCHOOL_MAIL_FROM"] ? {
    apiKey: schoolMailKey, from: process.env["BACKTOSCHOOL_MAIL_FROM"],
  } : undefined;
  const events = pool ? new LiveEvents(pool) : undefined;
  const layouts = pool ? new Layouts(pool) : undefined;
  const rooms = pool ? new Rooms(pool) : undefined;
  const trollbox = pool ? new Trollbox(pool) : undefined;
  // Tickets need somewhere for the money to go and a key to settle it with.
  // Without a CoinPay key every event is simply a free one.
  const ticketConfig = events
    ? ticketsFromEnv(process.env["NIXAMP_SITE"] ?? DEFAULT_DIRECTORY)
    : null;
  const tickets = ticketConfig ? new Tickets(ticketConfig) : undefined;

  // Accounts live where the directory lives, and only there: a nixamp on a
  // laptop has nobody to be an account of. Made here rather than inline in
  // the handler options because the authorization server issues its access
  // tokens out of the same table -- an OAuth token IS a nixamp token with a
  // client's name on it, which is why every existing route understands one.
  const accounts =
    options.directory && databaseUrl
      ? new Accounts({
          connectionString: databaseUrl,
          secret: process.env["NIXAMP_JWT_SECRET"] ?? "",
        })
      : undefined;
  // The ear lives where the accounts live. Loaded now, in the background, so
  // the first person to speak does not wait for the model to arrive; a box
  // without the optional model never says it is ready, and answers 503.
  const speech = accounts && process.env["NIXAMP_STT"] !== "off" ? new Speech() : undefined;
  // Account servers meter paid providers by default, including when checkout
  // is temporarily unconfigured. Self-hosters can explicitly sponsor usage.
  const translationPasses = accounts && pool && process.env["NIXAMP_TRANSLATION_BILLING"] !== "off"
    ? new TranslationPasses({ db: pool, key: coinpayKeyFrom(), site: nixampSite }) : undefined;
  // Payments to the copyright holders nixamp.com collects for, on its own CoinPay business.
  const supportPayments = accounts ? new Support({ key: coinpayKeyFrom(), site: nixampSite, holders: supportHoldersFrom() }) : undefined;
  // Monthly support: each month a new CoinPay payment and its link by email.
  const supportMonthly = supportPayments && supportPayments.enabled && pool ? new SupportSubscriptions({
    db: pool as unknown as import("./support.ts").Db,
    support: supportPayments,
    mail: process.env["RESEND_API_KEY"] ? { apiKey: process.env["RESEND_API_KEY"], from: process.env["NIXAMP_MAIL_FROM"] ?? "nixamp <notifications@nixamp.com>" } : null,
    site: nixampSite,
    log: (line) => console.log(line),
  }) : undefined;
  supportMonthly?.startSchedule();
  // The AI label's songs for sale: purchases kept here, the music zipped by the server that holds it.
  const jwtSecret = process.env["NIXAMP_JWT_SECRET"] ?? "";
  // The servers whose music nixamp.com sells and plays: the directory and the guide's peers.
  // Remembered in Postgres too, so the catalogue and owners are there the moment a deploy restarts us.
  const knownServers = options.directory && pool ? new KnownServers(pool as unknown as import("./veo-studio.ts").Queryable) : undefined;
  const listedServers = (): Array<{ base: string; ownerId: string }> =>
    directory ? directory.list().map((one) => ({ base: peerBase(one.url), ownerId: one.ownerId ?? "" })).filter((one) => one.base) : [];
  if (knownServers) {
    void knownServers.load().catch((error: unknown) => console.log(`  music servers not remembered: ${error instanceof Error ? error.message : String(error)}`));
    setInterval(() => void knownServers.sync(listedServers()).catch(() => undefined), 60_000).unref();
  }
  const musicServers = (): string[] => {
    const listed = listedServers().map((one) => one.base);
    return [...new Set([...guidePeers(), ...listed, ...(knownServers?.list().map((one) => one.base) ?? [])])];
  };
  // nixamp.com/music: every server's label in one catalogue (music-hub.ts).
  const musicHub = options.directory ? new MusicHub({ peers: musicServers }) : undefined;
  const musicStore = supportPayments && supportPayments.enabled && pool && jwtSecret ? new MusicStore({
    db: pool as unknown as import("./support.ts").Db,
    support: supportPayments,
    mail: process.env["RESEND_API_KEY"] ? { apiKey: process.env["RESEND_API_KEY"], from: process.env["NIXAMP_MAIL_FROM"] ?? "nixamp <notifications@nixamp.com>" } : null,
    site: nixampSite,
    secret: sha("sha256").update(`music-grant:${jwtSecret}`).digest("hex"),
    servers: musicServers,
    ...(musicHub ? { resolveServer: (id: string) => musicHub.resolve(id) } : {}),
    log: (line) => console.log(line),
  }) : undefined;
  // Licences to use that music in games, apps and videos (music-license.ts), paid the same way.
  const musicLicenses = musicStore && supportPayments && pool ? new MusicLicenses({
    db: pool as unknown as import("./support.ts").Db,
    support: supportPayments,
    store: musicStore,
    mail: process.env["RESEND_API_KEY"] ? { apiKey: process.env["RESEND_API_KEY"], from: process.env["NIXAMP_MAIL_FROM"] ?? "nixamp <notifications@nixamp.com>" } : null,
    site: nixampSite,
    serverId,
    log: (line) => console.log(line),
  }) : undefined;
  /**
   * A server says an album (or song) of its label landed: its artist's
   * followers are told. Only the account that owns a listed server may say
   * so, only for an artist that server's catalogue has, once per release.
   */
  // Veo shots for label music videos, under a monthly dollar cap (veo-studio.ts). nixamp.com only.
  const veoStudio = options.directory && process.env["NIXAMP_VEO_API_KEY"] ? new VeoStudio({
    key: process.env["NIXAMP_VEO_API_KEY"],
    // The month's spend in Postgres: a deploy recreates the container, and a file there would forget it.
    ledger: pool ? pgLedger(pool as unknown as import("./veo-studio.ts").Queryable) : fileLedger(VeoStudio.fileFor(stateDir(), options.port)),
    monthlyUsd: Number(process.env["NIXAMP_VEO_MONTHLY_USD"] ?? 50),
    usdPerSecond: Number(process.env["NIXAMP_VEO_USD_PER_SECOND"] ?? 0.15),
  }) : undefined;
  /** The signed-in account owns a server listed in the directory at this base. */
  const ownsListed = async (request: IncomingMessage, base: string): Promise<boolean> => {
    const me = accounts ? await accounts.whoIs(tokenFrom(request.headers)).catch(() => null) : null;
    if (!me || !base) return false;
    const listed = listedServers();
    // A server listed now answers for itself; one not (yet) back since a restart, by who last owned it.
    if (listed.some((one) => one.base === base)) return listed.some((one) => one.base === base && one.ownerId === me.id);
    return Boolean(knownServers?.list().some((one) => one.base === base && one.ownerId === me.id));
  };
  const musicVeo = async (request: IncomingMessage): Promise<{ status: number; body: unknown; video?: Buffer }> => {
    if (!veoStudio) return { status: 404, body: { error: "Veo is not set up here" } };
    const base = peerBase(String(request.headers["x-nixamp-server"] ?? ""));
    if (!(await ownsListed(request, base))) return { status: 403, body: { error: "only the owner of a listed server" } };
    if (request.method === "GET") return { status: 200, body: await veoStudio.status() };
    if (request.method !== "POST") return { status: 405, body: { error: "GET or POST" } };
    let input: { prompt?: unknown; seconds?: unknown; references?: unknown };
    try { input = JSON.parse(await readBody(request, 2 * 1024 * 1024)) as typeof input; } catch { return { status: 400, body: { error: "a JSON body under 2 MB" } }; }
    const prompt = String(input.prompt ?? "").trim();
    if (prompt.length < 20) return { status: 400, body: { error: "describe the shot" } };
    const references = (Array.isArray(input.references) ? input.references : []).slice(0, 3).map((one) => Buffer.from(String(one), "base64")).filter((one) => one.length > 1000);
    try {
      return { status: 200, body: null, video: await veoStudio.clip({ prompt, seconds: Number(input.seconds ?? 8), references }) };
    } catch (error) {
      if (error instanceof VeoBudgetError) return { status: 402, body: { error: error.message, code: "budget", ...(await veoStudio.status()) } };
      return { status: 422, body: { error: (error as Error).message, code: "refused" } };
    }
  };
  const announced = new Set<string>();
  const musicReleased = async (request: IncomingMessage, body: Record<string, unknown>): Promise<{ status: number; body: unknown }> => {
    const me = accounts ? await accounts.whoIs(tokenFrom(request.headers)).catch(() => null) : null;
    if (!me) return { status: 401, body: { error: "sign in as the server's owner" } };
    const base = peerBase(String(body["server"] ?? ""));
    const owns = directory?.list().some((one) => one.ownerId === me.id && peerBase(one.url) === base) ?? false;
    if (!base || !owns || !musicHub) return { status: 403, body: { error: "that server is not yours, or not listed" } };
    const catalogue = await musicHub.catalogue();
    const id = serverId(base);
    const artist = catalogue.genres.flatMap((one) => one.artists).find((one) => one.server === id && one.key === body["artist"]);
    const what = String(body["title"] ?? "").replace(/[\r\n]/g, " ").slice(0, 120);
    if (!artist || !what) return { status: 404, body: { error: "no such artist on that server" } };
    const key = `${id}:${artist.key}:${what}`;
    if (announced.has(key)) return { status: 200, body: { told: false, why: "already" } };
    announced.add(key);
    tellAudienceOf(artist.follow, {
      title: `New from ${artist.name}`,
      body: body["kind"] === "song" ? `"${what}" is out. Listen free on nixamp.` : `The album "${what}" is out. Listen free on nixamp.`,
      url: `${nixampSite}/music/artist/${id}/${artist.key}`,
    });
    return { status: 202, body: { told: true, follow: artist.follow } };
  };
  if (translationPasses) void translationPasses.access().catch(() => console.error("nixamp: translation checkout could not warm; it will retry on demand"));
  const liveVoice = accounts && process.env["NIXAMP_DUBBING"] !== "off" ? new LiveVoice({
    ...(pool ? { db: pool } : {}),
    ...(translationPasses ? { billing: translationPasses } : {}),
    dailyChars: Number(process.env["NIXAMP_DUB_DAILY_CHARS"] ?? 200_000),
    dailyAudioSeconds: Number(process.env["NIXAMP_DUB_DAILY_AUDIO_SECONDS"] ?? 86_400),
    userDailyChars: Number(process.env["NIXAMP_DUB_USER_DAILY_CHARS"] ?? 120_000),
    userDailyAudioSeconds: Number(process.env["NIXAMP_DUB_USER_DAILY_AUDIO_SECONDS"] ?? 43_200),
  }) : undefined;
  if (speech) {
    void speech.warm().then((ready) => {
      console.error(ready ? `nixamp: hearing with ${speech.model}` : `nixamp: not hearing: ${speech.lastFailure}`);
    });
  }
  // Translation lives beside the ear: the same optional library, a model
  // per pair of languages, loaded when first asked for. NIXAMP_MT_WARM
  // names pairs to load at boot (en-de,en-sv) so the first line does not
  // wait; NIXAMP_MT=off turns it off.
  const translator = accounts && process.env["NIXAMP_MT"] !== "off" ? new Translator() : undefined;
  const sharedTranslations = liveVoice && translator ? new SharedTranslations({ voice: liveVoice, translator, ...(translationPasses ? { billing: translationPasses } : {}) }) : undefined;
  if (translator) {
    const warm = (process.env["NIXAMP_MT_WARM"] ?? "").split(",").map((one) => one.trim()).filter(Boolean);
    if (warm.length > 0) {
      void translator.warm(warm).then((ready) => {
        console.error(ready ? `nixamp: translating (${warm.join(", ")} loaded)` : `nixamp: not translating everything: ${translator.lastFailure}`);
      });
    }
  }
  // What was heard, kept: where the accounts are, under the media's identity.
  const transcripts = pool ? new Transcripts(pool, (message) => console.log(message)) : undefined;
  const translations = transcripts ? new StoredTranslations(transcripts, translator, (message) => console.log(message)) : undefined;
  // The record of every file nixamp has met, by its bytes: nixamp.com/hash/<id>.
  const records = pool ? new Media(pool, (message) => console.log(message)) : undefined;

  // nixamp as an OAuth 2.1 authorization server, and the watch parties a
  // client site bridges through it. Both need the same three things -- a
  // database, accounts, and a site to be the issuer of -- so both appear or
  // neither does.
  const authServer =
    pool && accounts?.tokens
      ? new AuthorizationServer({
          db: pool,
          tokens: accounts.tokens,
          clients: clientsFrom(process.env),
          issuer: nixampSite,
        })
      : undefined;
  const parties =
    pool && events && authServer
      ? new WatchParties({
          db: pool,
          events,
          site: nixampSite,
          // A client may only point a watch link at its own site, which is
          // read off the redirect URIs it registered rather than configured
          // twice. Without it, "come and watch" could be sent anywhere.
          hostsFor: (origin) => {
            const client = authServer.client(origin);
            if (!client) return [];
            const hosts = new Set<string>();
            for (const uri of client.redirectUris) {
              try {
                hosts.add(new URL(uri).hostname);
              } catch {
                // A malformed registration names no host, which allows none.
              }
            }
            return [...hosts];
          },
        })
      : undefined;

  // Names and certificates for signed-in servers, and the rate limit over
  // everything. All of it is nixamp.com's business: the DNS keys live only
  // here, the certificates are issued here, and a personal nixamp has neither
  // a database nor strangers to meter. Without the registrar's keys the names
  // are simply not offered, rather than written into a zone that does not
  // exist.
  const zoneName = (() => {
    try {
      return new URL(process.env["NIXAMP_SITE"] ?? DEFAULT_DIRECTORY).hostname;
    } catch {
      return "nixamp.com";
    }
  })();
  const porkbunKey = process.env["PORKBUN_API_KEY"] ?? "";
  const porkbunSecret = process.env["PORKBUN_SECRET_API_KEY"] ?? "";
  const zone: DnsZone | null = porkbunKey && porkbunSecret ? new Porkbun(zoneName, porkbunKey, porkbunSecret) : null;
  const names = pool && zone ? new Names(pool, zone) : undefined;
  let certs: Certs | undefined;
  if (pool && zone) {
    const issuer = new AcmeIssuer({
      directoryUrl: process.env["NIXAMP_ACME_DIRECTORY"] ?? "https://acme-v02.api.letsencrypt.org/directory",
      email: process.env["NIXAMP_ACME_EMAIL"] ?? `hostmaster@${zoneName}`,
      // The key is kept by the store, so the issuer asks for it each time
      // rather than holding one that a second instance would not share.
      accountKey: () => (certs as Certs).accountKey(),
    });
    certs = new Certs(pool, zone, issuer, { log: (line) => console.log(`  ${line}`) });
  }
  const throttle = pool
    ? createThrottle({
        rules: [
          // Sign-in stays address-bucketed however the request is dressed, or
          // a guess with an Authorization header buys itself the bigger budget.
          { path: "/api/v1/auth/", limit: 20, credential: false },
          { path: "/api/v1/dns/", limit: 30 },
          // A text costs money and lands on a phone: ten a minute is plenty.
          { path: "/api/v1/invite", limit: 10 },
          { path: "/api/v1/dns", limit: 30 },
          { path: "/api/v1/certs", limit: 30 },
          // A token endpoint is where a stolen code or refresh token would be
          // tried, so it is address-bucketed: a client presenting its own id
          // must not get the credentialed budget to guess with.
          { path: "/api/v1/oauth/token", limit: 30, credential: false },
          { path: "/api/v1/oauth/", limit: 60, credential: false },
          { path: "/api/v1/watch-parties", limit: 60 },
          { path: "/api/health", open: true },
          { path: "/api/directory", limit: 120 },
        ],
        // A signed-in browser carries its session as a cookie, and is a
        // credential the same as a bearer token: a person on a dashboard is
        // not an anonymous scraper.
        credentialFrom: (request) =>
          presentedCredential(request.headers) ?? (tokenFrom(Object.fromEntries(request.headers)) || null),
      })
    : undefined;
  // The two things that were promises kept only in memory: a caller who was
  // told they would be texted, and the ended stream a code still points at.
  const durable = pool ? new Durable(pool, (message) => console.log(message)) : undefined;

  // Read at run time, and null unless both halves are set.
  const vapidKeys = vapidKeysFromEnv(process.env);

  /**
   * Tell a broadcaster's followers, on whatever they asked to be told on.
   *
   * Fired from the directory on the transition to live rather than on every
   * heartbeat, and awaited by nobody: a publisher's heartbeat should not sit
   * waiting on a push service.
   */
  const tellFollowers = (listing: Listing): void => {
    if (follows === undefined || !listing.ownerId) return;
    const what = listing.nowPlaying ? ` Playing ${listing.nowPlaying}.` : "";
    const note: Notification = {
      title: `${listing.name} is live`,
      body: `${what} Listen at ${DEFAULT_DIRECTORY}/directory, or call ${CALL_IN_NUMBER} and key ${listing.code}.`.trim(),
      url: listing.url,
    };
    tellAudienceOf(listing.ownerId, note);
  };

  /** Everyone following `followed` (an account, or `artist:<slug>`), by every way they asked to be told. */
  const tellAudienceOf = (followed: string, note: Notification): void => {
    if (follows === undefined) return;
    void follows
      .audience(followed)
      .then((audience) =>
        notifyAll(audience, note, {
          ...(process.env["RESEND_API_KEY"]
            ? {
                email: resendEmail({
                  apiKey: process.env["RESEND_API_KEY"],
                  from: process.env["NIXAMP_MAIL_FROM"] ?? "nixamp <notifications@nixamp.com>",
                  onEvent: (message) => console.log(message),
                }),
              }
            : {}),
          ...(process.env["TELNYX_API_KEY"] && process.env["PARTYLINE_SMS_FROM"]
            ? {
                sms: telnyxSms({
                  apiKey: process.env["TELNYX_API_KEY"],
                  from: process.env["PARTYLINE_SMS_FROM"],
                  onEvent: (message) => console.log(message),
                }),
              }
            : {}),
          ...(vapidKeys
            ? {
                push: webPush({
                  ...vapidKeys,
                  subject: process.env["NIXAMP_SITE"] ?? DEFAULT_DIRECTORY,
                  onEvent: (message) => console.log(message),
                }),
              }
            : {}),
          // A subscription the vendor has retired is a row to delete, not a
          // failure to retry.
          onGone: (endpoint) => follows.removePush(endpoint),
          onEvent: (message) => console.log(message),
        }),
      )
      .catch(() => {});
  };

  // Scheduled event reminders are sent once, three minutes before start.
  // Followers already have email/web preferences, so the same audience and
  // delivery paths serve live, update, and reminder notifications.
  const notifiedEvents = new Set<string>();
  const eventMail = schoolMail ?? (process.env["RESEND_API_KEY"] ? {
    apiKey: process.env["RESEND_API_KEY"],
    from: process.env["NIXAMP_MAIL_FROM"] ?? "nixamp <notifications@nixamp.com>",
  } : undefined);
  const notifyEventFollowers = (event: LiveEvent, kind: "updated" | "starting-soon"): void => {
    if (!follows || !event.ownerId) return;
    const key = `${event.id}:${kind}:${kind === "updated" ? event.version : event.startsAt ?? ""}`;
    if (notifiedEvents.has(key)) return;
    notifiedEvents.add(key);
    const note: Notification = {
      title: kind === "starting-soon" ? `${event.title} starts in 3 minutes` : `${event.title} was updated`,
      body: kind === "starting-soon" ? "Your followed host is about to start. Join now." : "A followed host changed the schedule or details.",
      url: `${nixampSite}/live/${encodeURIComponent(event.slug)}`,
    };
    void follows.audience(event.ownerId).then((audience) => notifyAll(audience, note, {
      ...(eventMail ? { email: resendEmail({ ...eventMail, onEvent: (message) => console.log(message) }) } : {}),
      ...(vapidKeys ? { push: webPush({ ...vapidKeys, subject: nixampSite, onEvent: (message) => console.log(message) }) } : {}),
      onGone: (endpoint) => follows.removePush(endpoint),
      onEvent: (message) => console.log(message),
    })).catch(() => undefined);
  };

  if (events && follows) {
    const checkScheduledEvents = async (): Promise<void> => {
      const now = Date.now();
      const upcoming = await events.list({ status: "scheduled", from: new Date(now - 4 * 60 * 1000).toISOString(), limit: 100 }).catch(() => []);
      for (const event of upcoming) {
        if (!event.startsAt) continue;
        const until = new Date(event.startsAt).getTime() - now;
        if (until <= 3 * 60 * 1000 && until > -60 * 1000) notifyEventFollowers(event, "starting-soon");
      }
    };
    void checkScheduledEvents();
    setInterval(() => void checkScheduledEvents(), 30_000).unref();
  }

  // Hoisted rather than built inline, because the party line needs the same
  // instance: a second Directory would be a second set of stream codes, and
  // the one the phone looked in would never be the one the publishers reach.
  const directory = options.directory
    ? new Directory(undefined, undefined, undefined, tellFollowers)
    : undefined;

  if (directory && durable) {
    // Echoed rather than awaited: the directory answers from memory, so a
    // database that is briefly unreachable should cost the durability and not
    // the request.
    directory.persistTo({
      save: (item) => void durable.saveEnded(item),
      drop: (id) => void durable.dropEnded(id),
    });
    // And put back what the last process knew, without holding up the listen.
    void durable
      .loadEnded(Date.now() - ENDED_TTL_MS)
      .then((items) => {
        if (items.length > 0) console.log(`  remembered ${items.length} stream(s) that had ended.`);
        directory.seedEnded(items);
      })
      .catch(() => {});
    void durable.sweep(Date.now() - ENDED_TTL_MS, new Date(Date.now() - 30 * 24 * 60 * 60 * 1000));
  }

  const session = readSession();
  // Captions for a channel: this server's ffmpeg turns the sound into PCM
  // and nixamp.com's ear, asked as this server, turns that into lines. A
  // server without ffmpeg cannot; a server nobody signed in on is refused
  // by the ear, and says so to whoever asks.
  const captions = tools.carries
    ? new Captions({
        ffmpeg: tools.ffmpeg,
        listen: (id, listener) => channels.listen(id, listener),
        session: () => readSession(),
        onEvent: (message) => console.log(`  ${message}`),
        // What a channel is playing, for the transcript store: a film by
        // its bytes and where it has got to, a link by its address, and
        // anything live as the broadcast it is.
        mediaOf: (id) => {
          const info = channels.info(id);
          if (!info) return null;
          const base = { title: info.name, startedAt: info.startedAt, backlog: info.codecs?.video ? BACKLOG_SECONDS : 3 };
          if (info.via === "pull" && !info.live && typeof info.position === "number" && info.source) {
            if (/^https?:\/\//.test(info.source)) {
              // The pasted address while it is still known: it outlives a media URL with a token in it.
              let pasted = info.source;
              for (const [link] of links) if (linkChannelId(link) === id) pasted = link;
              return { ...base, media: mediaOfUrl(pasted), position: info.position };
            }
            try {
              const source = info.source;
              return {
                ...base,
                media: fileFingerprint(source),
                position: info.position,
                // The file itself, for nixamp.com/hash/<id>: hashed here, once,
                // with what ffprobe and nichedb said and where it is on the air.
                describe: async () => {
                  const listen = publishable_ ? shareLink(publishable_.url, listenKey, false) : (options.publicUrl || `http://${options.host}:${options.port}`);
                  const described = await describeFile(source, {
                    tools,
                    enricher: enricher ?? null,
                    holder: { kind: "gateway", url: listen, seenAt: new Date().toISOString(), channel: id, name: streamName },
                  });
                  try {
                    remember(source, { id: described.id, fingerprint: described.fingerprint, size: described.size, mtimeMs: described.mtimeMs });
                  } catch {
                    // The index is a convenience.
                  }
                  return { id: described.id, keep: described.keep };
                },
              };
            } catch {
              return null;
            }
          }
          const self = publishable_?.url || options.publicUrl || `${options.host}:${options.port}`;
          return { ...base, media: mediaOfLive(self, id, info.startedAt) };
        },
      })
    : undefined;
  // The files this machine has told nixamp.com about, looked at again on
  // their own schedule (media-index.ts): a changed file is a new record.
  if (tools.carries) {
    const watch = async (): Promise<void> => {
      const signed = readSession();
      if (!signed) return;
      const counts = await watchOnce({
        refresh: (file, before) => refreshChanged(signed, file, before, { tools, enricher: enricher ?? null }),
        onEvent: (message) => console.log(`  ${message}`),
      }).catch((error: unknown) => {
        console.log(`  the media index could not be read: ${(error as Error).message}`);
        return null;
      });
      if (counts && counts.changed > 0) console.log(`  ${counts.changed} file(s) changed since nixamp.com last heard of them; told it`);
    };
    const watching = setInterval(() => void watch(), WATCH_EVERY_MS);
    watching.unref?.();
    setTimeout(() => void watch(), 60_000).unref?.();
  }
  const owner = new Owner({
    ownerId: options.owner || (session?.token ? await ownerIdOf(session) : ""),
    site: session?.site ?? DEFAULT_DIRECTORY,
  });

  // The party line answers a phone number, and there is only one number. Both
  // keys or neither: without the public key every webhook would be refused,
  // which is a worse failure than not offering the endpoint.
  const partyLine =
    options.directory && process.env["TELNYX_API_KEY"] && process.env["TELNYX_PUBLIC_KEY"]
      ? new PartyLine({
          apiKey: process.env["TELNYX_API_KEY"],
          publicKey: process.env["TELNYX_PUBLIC_KEY"],
          streams: directory,
          callIn: CALL_IN_NUMBER,
          // Only when a sending number is configured. Without one the line
          // still answers and still says when the stream ended; it just does
          // not offer a text it could not send.
          ...(process.env["PARTYLINE_SMS_FROM"]
            ? {
                sms: telnyxSms({
                  apiKey: process.env["TELNYX_API_KEY"],
                  from: process.env["PARTYLINE_SMS_FROM"],
                  onEvent: (message) => console.log(message),
                }),
              }
            : {}),
          ...(process.env["PARTYLINE_GREETING"] ? { greeting: process.env["PARTYLINE_GREETING"] } : {}),
          ...(process.env["PARTYLINE_VOICE"] ? { voice: process.env["PARTYLINE_VOICE"] } : {}),
          onEvent: (message) => console.log(message),
        })
      : undefined;

  if (partyLine && durable) {
    // Put back everybody a previous process promised to text, then keep
    // echoing. Seeding first means a stream that goes live during startup
    // still finds them.
    void durable
      .loadReminders()
      .then((waiting) => {
        const owed = [...waiting.values()].reduce((n, set) => n + set.size, 0);
        if (owed > 0) console.log(`  ${owed} caller(s) are still owed a text.`);
        partyLine.persistRemindersTo(
          {
            add: (code, phone) => void durable.addReminder(code, phone),
            take: (code) => durable.takeReminders(code),
          },
          waiting,
        );
      })
      .catch(() => {
        // Still worth echoing new ones even if the old list could not be read.
        partyLine.persistRemindersTo({
          add: (code, phone) => void durable.addReminder(code, phone),
          take: (code) => durable.takeReminders(code),
        });
      });
  }

  // Read before listening, so a missing or unreadable certificate is a sentence
  // now rather than a connection that resets later.
  let tls: { cert: string; key: string } | undefined = options.tlsCert
    ? (() => {
        try {
          return { cert: readFileSync(options.tlsCert, "utf8"), key: readFileSync(options.tlsKey, "utf8") };
        } catch (error) {
          throw new Error(`nixamp serve: could not read the certificate: ${(error as Error).message}`);
        }
      })()
    : undefined;

  // A signed-in server names itself.
  //
  // Nothing about DNS or certificates reaches this machine: it asks nixamp.com
  // for `<label>.<handle>.<zone>` pointing at the address it is calling from,
  // and for the handle's wildcard certificate, and serves https under that
  // name. The registrar's keys stay on nixamp.com. Skipped when the operator
  // named or certified the server by hand, when it listens on one interface
  // only, or with --no-name.
  let certExpiresAt = 0;
  let namedHost = "";
  const namedSession = readSession();
  if (
    !options.noName && !options.publicUrl && !options.tlsCert &&
    (options.host === "0.0.0.0" || options.host === "::") && namedSession?.token
  ) {
    const say = (line: string): void => console.log(`  ${line}`);
    const named = await claimName(namedSession.site, namedSession.token, labelFor(options.name, hostname()), say);
    if (named) {
      namedHost = named.host;
      // The certificate is the handle's, so the cache is keyed by the handle's
      // wildcard rather than by this machine's label.
      const wildcard = `*.${named.host.split(".").slice(1).join(".")}`;
      let files = readCertFiles(stateDir(), wildcard);
      if (!files) {
        const got = await fetchCert(namedSession.site, namedSession.token, {}, say);
        if (got) {
          writeCertFiles(stateDir(), got);
          files = { cert: got.cert, key: got.key, expiresAt: got.expiresAt };
        }
      }
      if (files) {
        tls = { cert: files.cert, key: files.key };
        certExpiresAt = files.expiresAt;
      }
      options.publicUrl = `${tls ? "https" : "http"}://${named.host}:${options.port}`;
      console.log(`  This server is ${named.host}${tls ? "" : " -- no certificate yet, so http for now"}.`);
    }
  }

  // Filled in below, when the RTMP listeners are opened. Read through a
  // function so the handler sees the list rather than the empty array it was
  // built with.
  let publishUrls: { id: string; url: string }[] = [];

  // Declared up here, not beside the publishing below: the port opens before
  // that code runs, so an admin asking to go live in the first moments would
  // otherwise reach a binding that has not been initialised.
  let publisher: Publisher | null = null;
  let listing: Listing | null = null;
  let publishable_: { label: string; url: string } | undefined;

  const server = createServer(engine, {
    web,
    webSites,
    media: options.media,
    owner,
    channels,
    stations,
    library,
    importer,
    rememberChannels: remembering,
    catalogs,
    publishUrls: () => publishUrls,
    serverName: streamName,
    homeSource: root,
    live: {
      status: () => ({
        live: publisher !== null,
        code: listing?.code ?? "",
        channelCodes: listing?.channelCodes ?? {},
        name: listing?.name ?? streamName,
        url: listing?.url ?? (publishable_ ? shareLink(publishable_.url, listenKey, false) : ""),
        // Whether going live is even possible here. A laptop behind a router
        // with no address the world can reach cannot be listed, and a button
        // that could only fail is worse than one that is not offered.
        possible: publishable_ !== undefined,
      }),
      start: async () => {
        if (publisher === null) publisher = makePublisher();
        if (publisher === null) {
          return { live: false, code: "", name: "", url: "", error: "this machine has no address the world can reach" };
        }
        const first = await publisher.start();
        if (first === null) {
          // Nothing was listed, so nothing should claim to be: a publisher
          // left running here would heartbeat at a directory that refused it.
          await publisher.stop();
          publisher = null;
          return {
            live: false, code: "", name: "", url: "",
            error: `${DEFAULT_DIRECTORY} would not list this stream. Run \`nixamp login\` on that machine.`,
          };
        }
        listing = first;
        return { live: true, code: first.code, name: first.name, url: first.url };
      },
      stop: async () => {
        await publisher?.stop();
        publisher = null;
        listing = null;
      },
      announce: async () => {
        if (publisher === null) return;
        const renewed = await publisher.announce();
        if (renewed) listing = renewed;
      },
    },
    ...(ingest ? { ingest } : {}),
    broadcaster,
    broadcast: () => ({ destinations, settings: DEFAULT_ENCODER }),
    version,
    key,
    listenKey,
    connections,
    paywall,
    ffmpeg: tools.ffmpeg,
    ffprobe: tools.ffprobe,
    // For a pasted link: where its media is, and the whole of it to keep.
    // A cookie jar beside the state, when the operator has put one there,
    // for the sites that will not talk to a datacenter without one.
    ytdlp: tools.ytdlp ?? null,
    carries: tools.carries !== false,
    cookies: cookiesFile(),
    hls,
    art,
    ...(captions ? { captions } : {}),
    compression,
    enricher,
    ...(tls ? { tls } : {}),
    // Untagged, so a directory of five thousand files answers at once; the
    // tags follow through `tag` below.
    load: (next) => loadSource(tools, next, false),
    tag: (next) => loadTagged(tools, next),
    ...(directory ? { directory } : {}),
    ...(follows ? { follows, vapidKeys } : {}),
    ...(favorites ? { favorites } : {}),
    ...(settingsSync ? { settingsSync } : {}),
    ...(events ? { events, eventWriter: new EventWriter({
      openaiKey: process.env["OPENAI_API_KEY"], anthropicKey: process.env["ANTHROPIC_API_KEY"],
      openaiModel: process.env["NIXAMP_WRITER_OPENAI_MODEL"], claudeModel: process.env["NIXAMP_WRITER_CLAUDE_MODEL"],
    }) } : {}),
    ...(events && follows ? { onEventUpdated: (updated: LiveEvent, previous: LiveEvent) => {
      if (updated.version !== previous.version) notifyEventFollowers(updated, "updated");
    } } : {}),
    ...(layouts ? { layouts } : {}),
    ...(rooms ? { rooms } : {}),
    ...(trollbox ? { trollbox } : {}),
    ...(speech ? { speech } : {}),
    ...(liveVoice ? { liveVoice } : {}),
    ...(translationPasses ? { translationPasses } : {}),
    ...(supportPayments ? { support: supportPayments } : {}),
    ...(supportMonthly ? { supportSubscriptions: supportMonthly } : {}),
    ...(musicStore ? { musicStore } : {}),
    ...(musicLicenses ? { musicLicenses } : {}),
    ...(musicHub ? { music: { hub: musicHub, released: musicReleased, veo: musicVeo } } : {}),
    ...(process.env["COINPAY_WEBHOOK_SECRET"] ? { coinpayWebhookSecret: process.env["COINPAY_WEBHOOK_SECRET"] } : {}),
    ...(sharedTranslations ? { sharedTranslations } : {}),
    ...(translator ? { translator } : {}),
    ...(transcripts ? { transcripts } : {}),
    ...(translations ? { translations } : {}),
    ...(records ? { records } : {}),
    site: nixampSite,
    ...(accounts && process.env["RESEND_API_KEY"] ? {
      resetMail: resendPasswordReset({
        apiKey: process.env["RESEND_API_KEY"],
        from: process.env["NIXAMP_MAIL_FROM"] ?? "nixamp <notifications@nixamp.com>",
        backtoschool: schoolMail,
      }),
    } : {}),
    // Other people's OpenProfiles, read for the voice a line is spoken in,
    // and the voices to speak in: ElevenLabs when the Telnyx account holds
    // the key for it (an integration secret named "elevenlabs"), Kokoro
    // otherwise. Nothing to configure on the deployment for either.
    ...(accounts ? { profiles: new Profiles() } : {}),
    ...(accounts && process.env["TELNYX_API_KEY"]
      ? { voices: new Voices({ telnyxApiKey: process.env["TELNYX_API_KEY"], onEvent: (message) => console.log(message) }) }
      : {}),
    ...(tickets ? { tickets } : {}),
    ...(authServer ? { authServer } : {}),
    ...(parties ? { parties } : {}),
    // Invites go out the same way follow notifications do, and only from a
    // site that has somebody to send them for.
    ...(pool
      ? {
          invites: {
            site: (process.env["NIXAMP_SITE"] ?? DEFAULT_DIRECTORY).replace(/\/+$/, ""),
            ...(process.env["RESEND_API_KEY"]
              ? {
                  email: resendEmail({
                    apiKey: process.env["RESEND_API_KEY"],
                    from: process.env["NIXAMP_MAIL_FROM"] ?? "nixamp <notifications@nixamp.com>",
                    backtoschool: schoolMail,
                    reason: "You received this email because a host invited you to join a live session.",
                    onEvent: (message) => console.log(message),
                  }),
                }
              : {}),
            ...(process.env["TELNYX_API_KEY"] && process.env["PARTYLINE_SMS_FROM"]
              ? {
                  sms: telnyxSms({
                    apiKey: process.env["TELNYX_API_KEY"],
                    from: process.env["PARTYLINE_SMS_FROM"],
                    onEvent: (message) => console.log(message),
                  }),
                }
              : {}),
          },
        }
      : {}),
    ...(names ? { names } : {}),
    ...(certs ? { certs } : {}),
    dnsZone: zoneName,
    ...(throttle ? { throttle } : {}),
    ...(partyLine ? { partyLine } : {}),
    // Accounts live where the directory lives, and only there: a nixamp on a
    // laptop has nobody to be an account of.
    ...(accounts
      ? {
          accounts,
          secureCookies: (process.env["NIXAMP_SITE"] ?? "").startsWith("https://"),
          // A deployment reached over https is one behind somebody's proxy, so
          // the socket address is that proxy and the forwarded header is the
          // caller. A nixamp on a laptop is reached directly and must not
          // believe a header anybody can send.
          behindProxy: (process.env["NIXAMP_SITE"] ?? "").startsWith("https://"),
          // Whichever providers this deployment was given both halves of, plus
          // the device grant, which is worth having even with no provider at
          // all: a browser already signed in can approve a terminal.
          // The same pool the follows and reminders use: three small tables in
          // one database do not want three sets of connections.
          ...(pool
            ? {
                servers: new Servers(pool),
                handles: new Handles(pool),
                openDirs: new OpenDirs(pool),
                accountProfiles: new AccountProfiles(pool),
                // The school's side of "Connect nixamp": tokens from nixamp.com,
                // kept against the school account. The issuer is this same
                // program in production, reached over HTTP like any client.
                links: new NixampLinks(pool, nixampExchange(nixampSite, BACKTOSCHOOL_CLIENT.id), BACKTOSCHOOL_CLIENT.id),
              }
            : {}),
          signIn: new SignIn(
            providersFrom(process.env),
            new DeviceGrants(),
            process.env["NIXAMP_SITE"] ?? DEFAULT_DIRECTORY,
          ),
        }
      : {}),
  });

  // A port already in use is the most ordinary failure there is, and it
  // arrives as an unhandled 'error' event that takes the process down with a
  // stack trace nobody reads.
  await new Promise<void>((done, fail) => {
    server.once("error", (error: NodeJS.ErrnoException) => {
      fail(
        new Error(
          error.code === "EADDRINUSE"
            ? `nixamp: port ${options.port} is already in use. Pass --port to pick another.`
            : error.code === "EACCES"
              ? `nixamp: not allowed to listen on port ${options.port}. Ports below 1024 need root.`
              : `nixamp: could not listen on ${options.host}:${options.port}: ${error.message}`,
        ),
      );
    });
    server.listen(options.port, options.host, done);
  });
  const bound = server.address();
  const port = typeof bound === "object" && bound !== null ? bound.port : options.port;

  // A named server keeps its certificate fresh without a restart: once a day
  // it asks for the handle's certificate again and, when a newer one has been
  // issued, swaps it into the running listener.
  if (namedHost && namedSession?.token) {
    const renew = setInterval(() => {
      void fetchCert(namedSession.site, namedSession.token, { waitMs: 0 }, () => undefined).then((got) => {
        if (!got || got.expiresAt <= certExpiresAt) return;
        writeCertFiles(stateDir(), got);
        certExpiresAt = got.expiresAt;
        const secure = server as unknown as { setSecureContext?: (context: { cert: string; key: string }) => void };
        secure.setSecureContext?.({ cert: got.cert, key: got.key });
        console.log(`  Renewed the certificate for ${namedHost}.`);
      });
    }, 24 * 60 * 60 * 1000);
    renew.unref();
  }

  const io = {
    read: readIfPossible,
    run: (command: string, args: string[]) => {
      const done = spawnSync(command, args, { encoding: "utf8" });
      return { status: done.status, stdout: done.stdout ?? "" };
    },
  };

  // The link, not the address. Without the key the address is a 401, so
  // printing a bare host:port would be printing something that does not work.
  //
  // Behind NAT no interface holds the public address, so if nobody said what it
  // is and nothing local looks public, ask. What comes back is a fact about the
  // router and not about this port -- the port still has to be forwarded -- so
  // it is marked as a guess and everything that prints it says so.
  const localAddresses = reachableAddresses(options.host, port, options.publicUrl, tls ? "https" : "http");
  const guessedPublic =
    options.lookup && !options.publicUrl && !localAddresses.some((a) => a.label === "on the internet")
      ? await lookupPublicIp()
      : "";
  const addresses = guessedPublic
    ? reachableAddresses(
        options.host,
        port,
        `${tls ? "https" : "http"}://${guessedPublic.includes(":") ? `[${guessedPublic}]` : guessedPublic}:${port}`,
        tls ? "https" : "http",
      )
    : localAddresses;

  // Listening on every interface proves the socket is open here and nothing
  // about the path between here and the phone.
  const listening = options.host === "0.0.0.0" || options.host === "::";
  const firewall = listening ? firewallInUse(io) : null;

  if (options.announce) {
    // Everything `nixamp daemon start` needs to print what this would have
    // printed. Without the addresses it could only reconstruct host and port,
    // which for a server bound to every interface means it printed 127.0.0.1 --
    // an address that works on exactly the machine you are already sitting at.
    // The firewall matters for the same reason: the warning was going into a
    // log file nobody reads instead of to the person who just typed the
    // command.
    console.log(
      JSON.stringify({
        nixamp: "listening",
        host: options.host,
        port,
        key,
        // The other half of the pair. Without it `nixamp admin` can only draw
        // the links that administer, so the operator has nothing to hand
        // somebody who should be able to watch and not to drive.
        listenKey,
        source: root,
        urls: addresses,
        firewall,
        guessedPublic: guessedPublic !== "",
      }),
    );
  }

  // Names now, tags later, and both after the door is open. Not awaited:
  // nothing below needs the library, and a directory that takes a minute to
  // walk should cost nobody a minute of not being able to connect.
  void loadSource(tools, root, false)
    .then((found) => {
      engine.fill(found, root);
      if (found.length === 0) {
        console.log(`nixamp serve — no audio files under ${root}`);
        return;
      }
      console.log(`nixamp serve — ${found.length} tracks under ${root}`);
      if (isRemote(root)) return;
      if (!homeSource) {
        // A playlist file or a single file: tagged as it always was.
        return loadTagged(tools, root, probeAsync, found.map((track) => track.path))
          .then((tagged) => engine.retag(tagged, root))
          .catch(() => undefined);
      }
      // What is already known, at once: a restart has its tags before the
      // first listener arrives. Then only new and changed files are asked
      // about, and their tags reach the player a couple of hundred at a time
      // rather than all at the end.
      retagPlayer(libraryIndex.tracksFor(found.map((track) => track.path)));
      const { done } = libraryIndex.scan(homeSource.id, probeFile, {
        onProgress: (changed) => retagPlayer(changed.filter((one) => one.state === "ready").map(trackOf)),
      });
      return done.then((report) => {
        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` : ""}.`);
      });
    })
    .catch((error: unknown) => {
      console.log(`nixamp: could not read ${root}: ${(error as Error).message}`);
    });

  console.log(`nixamp serve — reading ${root}`);
  console.log("");

  const width = Math.max(...addresses.map((a) => a.label.length));
  for (const { label, url } of addresses) {
    console.log(`  ${label.padEnd(width)}  ${shareLink(url, key)}`);
  }

  if (guessedPublic) {
    console.log("");
    console.log(`  That internet address is this machine's router, not this port.`);
    console.log(`  Nothing outside reaches it until ${port} is forwarded here.`);
  }

  console.log("");
  if (key === null) {
    console.log("  No key: anyone who can reach this port can drive it and hear it.");
  } else {
    console.log("  Open that link once on a phone or a laptop and it stays signed in.");
    console.log(`  Anything without the key gets a 401. Key: ${key}`);
    if (listenKey !== null) {
      console.log("");
      console.log("  A listen-only link, for someone you want to hear it but not drive it:");
      for (const { label, url } of addresses) {
        if (label === "here") continue;
        console.log(`    ${shareLink(url, listenKey, false)}`);
      }
    }
  }
  if (addresses.some((a) => a.label === "on the internet")) {
    console.log("");
    console.log(
      key === null
        ? "  The public address is open to anyone: --no-key means no key. --host 127.0.0.1 keeps it here."
        : "  The public address works from anywhere, for anyone with the key. --host 127.0.0.1 keeps it here.",
    );
  }
  if (!options.media) console.log("  Audio stays on this machine: --no-media is set.");
  if (owner.claimed) {
    console.log(`  ${session?.email} can administer this from anywhere, signed in at ${session?.site}.`);
  }
  if (options.ingest) console.log("  Accepting a live stream in at POST /api/ingest.");
  let rtmp: RtmpListeners | null = null;
  if (options.rtmpIn > 0) {
    const publish = addresses.find((a) => a.label !== "here") ?? addresses[0];
    const host = publish ? new URL(publish.url).hostname : "127.0.0.1";
    // One listener per stream, because ffmpeg's RTMP listener serves a single
    // connection per process. Three devices going live at once is three ports.
    const slots = Array.from({ length: options.rtmpStreams }, (_, i) => ({
      port: options.rtmpIn + i,
      id: i === 0 ? "live" : `live-${i + 1}`,
    }));
    rtmp = new RtmpListeners(channels, tools.ffmpeg, listenKey ?? "live");
    rtmp.listen(slots);

    publishUrls = slots.map((slot) => ({
      id: slot.id,
      url: `rtmp://${host}:${slot.port}/live/${listenKey ?? "live"}`,
    }));

    console.log("  Or publish from OBS, Larix or ffmpeg, one per URL:");
    for (const entry of publishUrls) {
      console.log(`    ${entry.url}   -> "${entry.id}"`);
    }
  }
  if (destinations.length > 0) {
    console.log(`  Ready to broadcast to ${destinations.map((d) => d.name).join(", ")}.`);
  }
  if (web === null) console.log("  No built PWA found, so / has nothing to serve: run `bun run web:build`.");

  let closePort: (() => void) | null = null;

  if (firewall !== null) {
    const { open, close } = portCommands(firewall, port);
    if (!options.openPort) {
      console.log("");
      console.log(`  ${firewall} is running, so other devices cannot reach this port yet:`);
      console.log(`    sudo ${open.join(" ")}`);
      console.log("  or start with --open-port and nixamp will do it, and undo it on exit.");
    } else {
      const elevated = elevate(io, open);
      if (elevated === null) {
        console.log("");
        console.log(`  --open-port needs root or passwordless sudo. Run this yourself:`);
        console.log(`    sudo ${open.join(" ")}`);
      } else {
        const done = spawnSync(elevated[0] as string, elevated.slice(1), { encoding: "utf8" });
        if (done.status === 0) {
          console.log("");
          console.log(`  Opened ${port}/tcp in ${firewall}. It closes again when this exits.`);
          // Leave the machine as it was found. A player should not be the
          // reason a port is still open next week.
          closePort = () => {
            const undo = elevate(io, close);
            if (undo) spawnSync(undo[0] as string, undo.slice(1), { stdio: "ignore" });
          };
        } else {
          console.log("");
          console.log(`  Could not open the port: ${(done.stderr || done.stdout || "").trim() || "unknown error"}`);
        }
      }
    }
  }

  // The listing carries the listen link, and only ever a public address: an
  // entry pointing at 192.168.1.5 is one nobody outside that house can open.
  // Never a bare IP over https: a certificate is issued for a name, so a
  // listing pointing at one is a listing nobody can open.
  publishable_ = addresses.find((a) => a.label === "on the internet" && certifiable(a.url))
    ?? addresses.find((a) => a.label === "on tailscale" && certifiable(a.url))
    ?? addresses.find((a) => a.label === "on the internet")
    ?? addresses.find((a) => a.label === "on tailscale");

  /**
   * Make a publisher for this server. Called at startup when the operator says
   * yes, and again whenever somebody goes live from the admin panel.
   *
   * A declaration rather than an assignment, so it exists from the moment the
   * function is entered -- the port is open well before this line is reached.
   */
  /** The station behind this channel, when its owner made it public and listed it. */
  function listedStation(channelId: string): Station | undefined {
    const station = stationStore.get(stationOfChannel(channelId));
    return station && station.visibility === "public" && station.listed ? station : undefined;
  }

  function makePublisher(): Publisher | null {
    if (!publishable_) return null;
    const listen = shareLink(publishable_.url, listenKey, false);
    // Announced next to the listen link, not instead of it: one is for a person
    // with a browser, the other for the phone line and anything else that is
    // handed one address and expected to play it.
    const audio = audioLink(publishable_.url, listenKey);
    return new Publisher({
        directory: DEFAULT_DIRECTORY,
        name: streamName,
        url: listen,
        audio,
        // The control link, for the owner to open this machine as its
        // administrator from the directory. The directory shows it to the
        // owning account and strips it for everyone else.
        ...(key ? { admin: shareLink(publishable_.url, key) } : {}),
        // Asked at every heartbeat rather than once, because the library is
        // read after the port opens and is still arriving when this is made.
        tracks: () => engine.snapshot(false).trackCount,
        // What a visitor would find here: whether the player is running, and
        // which channels are on. A directory row that says "5,717 files,
        // live: CNN" is one somebody can decide about; "5717 tracks" was not.
        playing: () => engine.snapshot(false).playing,
        // Stations only when their owner listed them; every other channel as before.
        channels: () => channels.list().filter((one) => !stationOfChannel(one.id) || listedStation(one.id)).map((one) => one.name),
        // The same with what a link preview needs: the id a join link names
        // a channel by, a picture, a line.
        lineup: () => channels.list()
          .filter((one) => !stationOfChannel(one.id) || listedStation(one.id))
          .map((one) => {
            const entry = lineupEntry(listen, one);
            const station = listedStation(one.id);
            // A station is listened to through its own link, never the server's.
            return station
              ? { ...entry, listen: `${publishable_!.url}/station/${station.id}?t=${encodeURIComponent(station.listenToken)}` }
              : entry;
          }),
        // From `nixamp login`. The directory will not list a stream it cannot
        // attribute to somebody, because a listing is now a phone code that
        // costs money to answer.
        ...(session?.token ? { token: session.token } : {}),
        onRefused: () => {
          console.log("");
          console.log("  nixamp.com would not list this stream: it needs an account.");
          console.log("  Run `nixamp login` (or `nixamp signup`) and start again.");
        },
        // Every heartbeat, so the code this server shows is the code the
        // phone line knows, whatever the directory has forgotten meanwhile.
        onListed: (fresh) => {
          if (listing && listing.code !== fresh.code) {
            console.log(`  nixamp.com listed this stream again; the phone code is now ${fresh.code}.`);
          }
          listing = fresh;
        },
        nowPlaying: () => {
          const snapshot = engine.snapshot();
          return snapshot.tracks?.[snapshot.index]?.title ?? "";
        },
        onConfig: (remote) => {
          const next = applyRemoteConfig(paywallConfig, (remote as { x402?: unknown })?.x402);
          if (JSON.stringify(next) === JSON.stringify(paywallConfig)) return;
          paywallConfig = next;
          console.log(next.enabled
            ? `  nixamp.com turned paid listening on: $${(next.priceCents / 100).toFixed(2)} for ${next.passMinutes} minutes, over ${FREE_LISTENERS} listeners.`
            : "  nixamp.com turned paid listening off.");
        },
    });
  }

  if (options.publish !== "no" && publishable_) {
    const listen = shareLink(publishable_.url, listenKey, false);
    // Listed unless told otherwise.
    //
    // It used to ask, and a daemon has nobody to ask -- so a server started in
    // the background was never listed, which meant nobody could find it, which
    // meant it could not be paid for either. A stream that nobody can find is
    // not a stream. What is published is the listening link, never the one
    // that drives, and `--no-publish` is there for a server that should not be
    // public at all.
    //
    // Still asked when a person is at the terminal to answer, because that is
    // somebody who can say no -- and since 0.29 the answer is kept, so a
    // daemon started later does what the person said, not what it guesses.
    const recorded = readConfig().publish;
    const choice = publishChoice(options.publish, recorded, Boolean(process.stdin.isTTY), established);
    const wanted = choice === "ask"
      ? await confirm(`\n  List this stream at ${DEFAULT_DIRECTORY}/directory so anyone can find it?\n  It publishes ${listen} — listen only, not the controls.`)
      : choice === "yes";
    if (shouldRecord(options.publish, recorded)) {
      try { writeConfig({ publish: wanted }); } catch { /* unwritable state: asked again next time */ }
    }
    if (!wanted && options.publish === "ask") {
      console.log("");
      console.log(`  Not listed at ${DEFAULT_DIRECTORY}/directory. Start with --publish to list it.`);
    }

    if (wanted) {
      publisher = makePublisher();
      listing = (await publisher?.start()) ?? null;
      console.log("");
      console.log(listing
        ? `  Listed at ${DEFAULT_DIRECTORY}/directory as "${listing.name}". It leaves the list when this stops.`
        : `  Could not reach ${DEFAULT_DIRECTORY}; not listed.`);
    }
  } else if (options.publish === "yes" && !publishable_) {
    console.log("");
    console.log("  --publish needs an address the world can reach. This machine has none.");
  }

  // Remember this machine on the account it belongs to, without being asked.
  //
  // `nixamp server add --here` existed and did exactly this, which is a manual
  // step for something the daemon already knows: it has just worked out its
  // own address and minted its own key, and the session file says whose it is.
  // Somebody who has signed in on this machine has said which account it is;
  // being on their list is what they meant by that.
  //
  // Idempotent: adding the same URL again updates the row rather than making
  // a second one, so this is safe on every start. Silent about failure, since
  // nothing here is worth stopping a player for.
  if (session?.token && publishable_) {
    const remembered = shareLink(publishable_.url, key);
    void fetch(`${DEFAULT_DIRECTORY}/api/v1/servers`, {
      method: "POST",
      headers: { "content-type": "application/json", authorization: `Bearer ${session.token}` },
      body: JSON.stringify({ url: publishable_.url, name: options.name || hostname(), key: key ?? "" }),
    })
      .then((answer) => {
        if (answer.ok) console.log(`  Remembered on your account at ${DEFAULT_DIRECTORY}.`);
      })
      .catch(() => {
        // The directory being unreachable is not a reason to stop serving.
      });
    void remembered;
  }

  // A last resort, not a licence.
  //
  // Every throw reachable from a request should be caught where it happens,
  // and one that is not is a bug worth fixing. But the alternative to catching
  // it here is that node prints a stack and exits -- and a music server
  // exiting drops every listener, forgets what was added, and leaves ffmpeg
  // children behind. That happened twice from one bad URL. Whatever is wrong
  // with one response, everybody else is still listening.
  process.on("uncaughtException", (error) => {
    console.error(`nixamp: kept going after an unexpected error: ${error.stack ?? error.message}`);
  });
  process.on("unhandledRejection", (reason) => {
    console.error(`nixamp: kept going after an unhandled rejection: ${String(reason)}`);
  });

  const shutdown = (): void => {
    rtmp?.stop();
    enricher.save();
    compression.stopAll();
    hls.stopAll();
    stationRunner?.close();
    channels.stopAll();
    ingest?.stopRtmp();
    ingest?.close();
    broadcaster.stop();
    void publisher?.stop();
    closePort?.();
    engine.stop();
    server.close(() => process.exit(0));
    // A hung keep-alive should not outlive a ctrl-c.
    setTimeout(() => process.exit(0), 1000).unref();
  };
  process.on("SIGINT", shutdown);
  process.on("SIGTERM", shutdown);
}

/**
 * `--rtmp youtube=<key>` or `--rtmp name=rtmp://host/app/key`.
 *
 * A key is a password, so it is taken from the command line or the environment
 * and never from a request: a client that could name its own destination could
 * point your broadcast at itself.
 */
export function parseDestinations(specs: string[]): Destination[] {
  const out: Destination[] = [];
  for (const [index, spec] of specs.entries()) {
    const at = spec.indexOf("=");
    if (at <= 0) continue;
    const name = spec.slice(0, at).trim();
    const rest = spec.slice(at + 1).trim();
    if (!name || !rest) continue;

    const preset = PRESETS[name.toLowerCase()];
    if (preset && !/^rtmps?:\/\//i.test(rest)) {
      out.push({ id: String(index + 1), name, url: preset, key: rest, enabled: true });
      continue;
    }
    if (!/^rtmps?:\/\//i.test(rest)) continue;

    // A full URL: the last path segment is the key.
    const cut = rest.lastIndexOf("/");
    if (cut <= "rtmp://".length) continue;
    out.push({
      id: String(index + 1),
      name,
      url: rest.slice(0, cut),
      key: rest.slice(cut + 1),
      enabled: true,
    });
  }
  return out;
}

/**
 * Which account the signed-in session belongs to. Asked once at startup rather
 * than trusted from the file: a token that nixamp.com no longer accepts should
 * not confer ownership of anything.
 */
async function ownerIdOf(session: { site: string; token: string }): Promise<string> {
  try {
    const answer = await fetch(`${session.site}/api/v1/auth/me`, {
      headers: { authorization: `Bearer ${session.token}` },
    });
    if (!answer.ok) return "";
    const body = (await answer.json()) as { account?: { id?: string } };
    return typeof body.account?.id === "string" ? body.account.id : "";
  } catch {
    // Offline at startup means no remote administration until a restart, and
    // the control key still works. Better than claiming an owner we cannot
    // check.
    return "";
  }
}

/** The built PWA, when it is sitting next to us in the same install. */
function defaultWebDir(): string | null {
  const fromEnv = process.env.NIXAMP_WEB_DIR;
  if (fromEnv && isFile(join(fromEnv, "index.html"))) return fromEnv;
  const here = fileURLToPath(new URL(".", import.meta.url));
  for (const guess of [
    join(here, "..", "web", "dist"),
    join(here, "..", "..", "web", "dist"),
    join(here, "..", "web"),
  ]) {
    if (isFile(join(guess, "index.html"))) return resolve(guess);
  }
  return null;
}
