@torrent-tv/proxy 2.64.0 → 2.64.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -42,6 +42,9 @@ Linux-only host (e.g. POSIX-only signals must degrade elsewhere).
42
42
  `routes/*` are now thin HTTP translators.
43
43
  - `container-index/` — legacy readers (ebml-reader, matroska/mp4/avi keyframe and
44
44
  subtitle tables) — used internally by `container/*`, deprecated as direct import.
45
+ - `docs/logs.md` — where to find logs (HA `docker logs` + `/data/proxy.log`, DO
46
+ `infra-server-1` with forwarded frontend `POST /api/client-logs`). Browser
47
+ console not needed.
45
48
  - `playback-planner.js` — single ffmpeg probe returns audioCodec, videoCodec,
46
49
  container, durationSeconds. `mode` is advisory; the browser decides.
47
50
  - `hls-session-manager.js` — one ffmpeg per (source, file, settings). Serves a
package/docs/logs.md ADDED
@@ -0,0 +1,43 @@
1
+ # Logs — where to look
2
+
3
+ All logs are visible via container `docker logs`, no browser console copy-paste needed.
4
+
5
+ ## HA proxy (aarch64, addon `b34a1737_torrent_tv_proxy`)
6
+
7
+ Image `b34a1737/aarch64-addon-torrent_tv_proxy:<version>` = `@torrent-tv/proxy` `<version>` (see `ha-addon/torrent_tv_proxy/config.yaml`).
8
+
9
+ - Console + file are the same: the proxy logs to both `stdout` and `/data/proxy.log` on start (`logging to /data/proxy.log as well as the console`).
10
+ - Via container:
11
+ ```bash
12
+ ssh ha "sudo docker logs app_b34a1737_torrent_tv_proxy --tail 200"
13
+ ssh ha "sudo docker exec app_b34a1737_torrent_tv_proxy cat /data/proxy.log | tail -n 300"
14
+ ```
15
+ - Filter by session: every transcode session logs its id, e.g. `003ed2fd-7c9b-4cd5-9d05-ff875ff2be23`, `hold segment-00068.mp4 failed`, `encode-run failed`, `EXITED_*`.
16
+ - Host file `/data/proxy.log` survives `docker logs` rotation; `core dumps: 1 present` line shows kept dump under `/data`.
17
+
18
+ SSH to HA requires `MACs hmac-sha2-256-etm@openssh.com,hmac-sha2-512-etm@openssh.com,umac-128-etm@openssh.com` — server `OpenSSH_10.3` on `homeassistant.local` offers only `*-etm` (`Unable to negotiate` / `Corrupted MAC` otherwise). Already in `~/.ssh/config` `Host ha`.
19
+
20
+ ## DO server (webauth.courses, `infra-server-1`)
21
+
22
+ - Server is `infra-server-1` (`ghcr.io/torrent-tv/server:latest`) on `do` (`206.189.97.152`).
23
+ - Frontend logs are forwarded: browser batches `{sessionId, tag, signalSessionId, lines: [{level,ts,msg}]}` and `POST`s to `https://webauth.courses/api/client-logs` (`server/routes/api/client-logs/post.js:56`), server does `console.log` with prefix `[client <tag> <id> sig=<webrtcSessionId>]`. They appear together with backend logs in the same container.
24
+ - Via container:
25
+ ```bash
26
+ ssh do "docker logs infra-server-1 --tail 200 | cat"
27
+ # filter a single viewing:
28
+ ssh do "docker logs infra-server-1 --tail 500 | grep 003ed2fd"
29
+ ssh do "docker logs infra-server-1 --tail 500 | grep '\[client'"
30
+ ```
31
+ - No need to open eruda or copy browser console on phone — it is already in `infra-server-1` logs.
32
+
33
+ ## Quick triage
34
+
35
+ - Rewind/seek bug (hold 0ms → 500): HA `hold ... failed after 0ms → 500` + `encode-run` lines for the same `<sessionId>`, and DO `[client ...] fragLoadError / levelLoadError 500` for same `sn`.
36
+ - Cushion / link budget: HA `memory: rss=... anon=...` and `cushion` lines; DO `[eta]` / `[cushion]` from client.
37
+ - Update check: `ssh ha "sudo docker exec hassio_cli ha apps info b34a1737_torrent_tv_proxy | grep version"` and `ssh ha "sudo docker ps --filter name=app_b34a1737_torrent_tv_proxy"`.
38
+
39
+ ## Related
40
+
41
+ - `ha-addon/CLAUDE.md` — addon build/update detour via `hassio_cli`, cache-bust via `config.yaml` version.
42
+ - `docs/container-architecture.md` — what container/track classes log and where.
43
+ - `server/routes/api/client-logs/post.js:1` — sanitization (control chars → space, `MAX_LINES 50`, `MAX_MSG_LEN 2000`).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@torrent-tv/proxy",
3
- "version": "2.64.0",
3
+ "version": "2.64.2",
4
4
  "description": "Torrent proxy client that exposes webseed-like HTTP stream endpoint.",
5
5
  "license": "GPL-3.0-or-later",
6
6
  "publishConfig": {
@@ -34,8 +34,13 @@ export async function handleApiSourceStatsGet(req, reply, { sourceRegistry, torr
34
34
  torrent = await torrentPool.getTorrent(sourceRecord.sourceType, sourceRecord.source);
35
35
  } catch (error) {
36
36
  const message = error instanceof Error ? error.message : String(error);
37
+ logger.warn(`stats: getTorrent failed for ${sourceKey.slice(0, 8)}: ${message} stack=${error instanceof Error ? error.stack?.split("\n")[1]?.trim() ?? "" : ""}`);
37
38
  return reply.code(500).send({ error: `Failed to load torrent: ${message}` });
38
39
  }
40
+ if (!torrent) {
41
+ logger.warn(`stats: torrent missing for ${sourceKey.slice(0, 8)} (getTorrent returned null/undefined)`);
42
+ return reply.code(500).send({ error: "Torrent instance not found" });
43
+ }
39
44
 
40
45
  const fileIndexRaw = typeof req.query.fileIndex === "string" ? req.query.fileIndex : "";
41
46
  const fileIndex = fileIndexRaw !== "" && /^\d+$/.test(fileIndexRaw) ? Number(fileIndexRaw) : null;
@@ -136,9 +136,9 @@ export class MatroskaContainer extends Container {
136
136
  case ID_LANGUAGE_BCP47: languageBcp47 = readString(head, f); break;
137
137
  case ID_NAME: name = readString(head, f); break;
138
138
  case ID_FLAG_ENABLED: isEnabled = f.size === 0 || readUint(head, f.dataOffset, f.size) !== 0; break;
139
- case ID_FLAG_DEFAULT: isDefault = readUint(head, f.dataOffset, f.size) === 1; declaresDefault = true; break;
139
+ case ID_FLAG_DEFAULT: isDefault = f.size === 0 || readUint(head, f.dataOffset, f.size) === 1; declaresDefault = true; break;
140
140
  case ID_FLAG_FORCED: isForced = f.size > 0 && readUint(head, f.dataOffset, f.size) !== 0; break;
141
- case ID_FLAG_HEARING: isHearing = f.size > 0 && readUint(head, f.dataOffset, f.size) !== 0; break;
141
+ case ID_FLAG_HEARING: isHearing = f.size > 0 && readUint(head, f.dataOffset, f.size) !== 0; break;
142
142
  case ID_FLAG_VISUAL: isVisual = f.size > 0 && readUint(head, f.dataOffset, f.size) !== 0; break;
143
143
  case ID_FLAG_TEXT_DESCR: break;
144
144
  case ID_FLAG_ORIGINAL: isOriginal = f.size > 0 && readUint(head, f.dataOffset, f.size) !== 0; break;
@@ -1191,7 +1191,7 @@ export class TorrentPool {
1191
1191
  `(~${(freed / (1024 * 1024)).toFixed(0)} MB)`
1192
1192
  );
1193
1193
  this.#cancelIdleRemoval(torrent);
1194
- this.#removeTorrent(torrent);
1194
+ this.#removeTorrent(torrent, "disk-cap");
1195
1195
  used -= freed;
1196
1196
  }
1197
1197
  }
@@ -1582,15 +1582,20 @@ export class TorrentPool {
1582
1582
  return;
1583
1583
  }
1584
1584
  this.#cancelIdleRemoval(torrent);
1585
+ const name = typeof torrent.name === "string" ? torrent.name : "(unknown)";
1586
+ const ih = String(torrent.infoHash ?? "?").slice(0, 8);
1587
+ logger.info(`torrent-pool: scheduling idle removal for "${name}" [${ih}] in ${TORRENT_IDLE_TTL_MS / 1000}s`);
1585
1588
  const timer = setTimeout(() => {
1586
1589
  this.#idleTimers.delete(torrent);
1587
1590
  // Re-check: a new acquire since scheduling would have cancelled this
1588
1591
  // timer, but guard anyway against a race.
1589
1592
  const usage = this.fileUsageByTorrent.get(torrent);
1590
1593
  if (usage && usage.size > 0) {
1594
+ logger.info(`torrent-pool: idle timer fired for "${name}" [${ih}] but refcount ${usage.size} >0 — keep`);
1591
1595
  return;
1592
1596
  }
1593
- this.#removeTorrent(torrent);
1597
+ logger.warn(`torrent-pool: idle TTL fired for "${name}" [${ih}] — removing torrent (reason=idle-ttl caller=scheduleIdleRemoval)`);
1598
+ this.#removeTorrent(torrent, "idle-ttl");
1594
1599
  }, TORRENT_IDLE_TTL_MS);
1595
1600
  timer.unref?.();
1596
1601
  this.#idleTimers.set(torrent, timer);
@@ -1615,12 +1620,20 @@ export class TorrentPool {
1615
1620
  * disk while the proxy keeps running. Best-effort.
1616
1621
  *
1617
1622
  * @param {import("webtorrent").Torrent} torrent
1623
+ * @param {string} [reason="unknown"] - Why removal was requested (idle-ttl, disk-cap, evict, api).
1618
1624
  * @returns {void}
1619
1625
  */
1620
- #removeTorrent(torrent) {
1626
+ #removeTorrent(torrent, reason = "unknown") {
1621
1627
  if (!torrent) {
1622
1628
  return;
1623
1629
  }
1630
+ const infoHash = String(torrent.infoHash ?? "?").slice(0, 8);
1631
+ const name = typeof torrent.name === "string" ? torrent.name : "(unknown)";
1632
+ const usage = this.fileUsageByTorrent.get(torrent);
1633
+ const refcount = usage ? usage.size : 0;
1634
+ const hasData = (() => { try { return torrentDownloadedBytes(torrent); } catch { return -1; } })();
1635
+ // Capture caller for diagnostics — not for control flow.
1636
+ const caller = new Error().stack?.split("\n")[2]?.trim() ?? "";
1624
1637
  // Drop it from the source→torrent map so a later request re-adds it.
1625
1638
  for (const [key, value] of this.torrents) {
1626
1639
  if (value === torrent) {
@@ -1631,14 +1644,14 @@ export class TorrentPool {
1631
1644
  this.fileUsageByTorrent.delete(torrent);
1632
1645
  this.#lastAccess.delete(torrent);
1633
1646
  this.#readPositionByTorrent.delete(torrent);
1634
- const name = typeof torrent.name === "string" ? torrent.name : "(unknown)";
1647
+ logger.warn(`torrent-pool: removing torrent "${name}" [${infoHash}] reason=${reason} refcount=${refcount} downloaded=${hasData}B caller=${caller}`);
1635
1648
  try {
1636
1649
  torrent.destroy({ destroyStore: true }, () => {
1637
- logger.info(`torrent-pool: removed idle torrent "${name}" and its store`);
1650
+ logger.info(`torrent-pool: removed torrent "${name}" [${infoHash}] reason=${reason} and its store`);
1638
1651
  });
1639
1652
  } catch (error) {
1640
1653
  const message = error instanceof Error ? error.message : String(error);
1641
- logger.warn(`torrent-pool: failed to remove idle torrent "${name}": ${message}`);
1654
+ logger.warn(`torrent-pool: failed to remove torrent "${name}" [${infoHash}] reason=${reason}: ${message}`);
1642
1655
  }
1643
1656
  }
1644
1657
 
@@ -1,14 +1,14 @@
1
1
  /**
2
2
  * @file Text subtitle track — convertible to WebVTT.
3
3
  *
4
- * Matroska: S_TEXT/UTF8, S_TEXT/ASS, S_TEXT/SSA
4
+ * Matroska: S_TEXT/UTF8, S_TEXT/ASS, S_TEXT/SSA, S_TEXT/WEBVTT (RFC 9559)
5
5
  * MP4: tx3g, text, wvtt (stpp/TTML is NOT text for this pipeline — excluded)
6
6
  * External files: .srt .ass .ssa .vtt — modelled as TextSubtitleTrack with no container backing.
7
7
  */
8
8
 
9
9
  import { SubtitleTrack } from "./SubtitleTrack.js";
10
10
 
11
- const TEXT_CODECS_MATROSKA = new Set(["S_TEXT/UTF8", "S_TEXT/ASS", "S_TEXT/SSA"]);
11
+ const TEXT_CODECS_MATROSKA = new Set(["S_TEXT/UTF8", "S_TEXT/ASS", "S_TEXT/SSA", "S_TEXT/WEBVTT"]);
12
12
  const TEXT_FORMATS_MP4 = new Set(["tx3g", "text", "wvtt"]);
13
13
 
14
14
  export class TextSubtitleTrack extends SubtitleTrack {