@zfadhli/tokrec 0.13.0 → 0.15.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.
Files changed (126) hide show
  1. package/README.md +99 -403
  2. package/dist/app/abort.mjs +14 -0
  3. package/dist/app/abort.mjs.map +1 -0
  4. package/dist/{lib → app}/config.d.mts +73 -71
  5. package/dist/app/config.d.mts.map +1 -0
  6. package/dist/app/config.mjs +26 -0
  7. package/dist/app/config.mjs.map +1 -0
  8. package/dist/app/events.mjs +36 -0
  9. package/dist/app/events.mjs.map +1 -0
  10. package/dist/app/job.mjs +190 -0
  11. package/dist/app/job.mjs.map +1 -0
  12. package/dist/{lib → app}/logger.mjs +1 -1
  13. package/dist/app/logger.mjs.map +1 -0
  14. package/dist/{core/monitor.mjs → app/loop.mjs} +19 -9
  15. package/dist/app/loop.mjs.map +1 -0
  16. package/dist/app/recorder.d.mts +7 -0
  17. package/dist/app/recorder.d.mts.map +1 -0
  18. package/dist/app/recorder.mjs +233 -0
  19. package/dist/app/recorder.mjs.map +1 -0
  20. package/dist/core/errors.d.mts +16 -0
  21. package/dist/core/errors.d.mts.map +1 -0
  22. package/dist/core/errors.mjs +15 -0
  23. package/dist/core/errors.mjs.map +1 -0
  24. package/dist/core/format.d.mts +12 -0
  25. package/dist/core/format.d.mts.map +1 -0
  26. package/dist/{lib/utils.mjs → core/format.mjs} +6 -41
  27. package/dist/core/format.mjs.map +1 -0
  28. package/dist/core/plan.mjs +52 -0
  29. package/dist/core/plan.mjs.map +1 -0
  30. package/dist/core/verdict.d.mts +44 -0
  31. package/dist/core/verdict.d.mts.map +1 -0
  32. package/dist/core/verdict.mjs +47 -0
  33. package/dist/core/verdict.mjs.map +1 -0
  34. package/dist/index.d.mts +6 -3
  35. package/dist/index.mjs +5 -2
  36. package/dist/{core/api/client.mjs → io/http.mjs} +38 -10
  37. package/dist/io/http.mjs.map +1 -0
  38. package/dist/io/media/convert.mjs +60 -0
  39. package/dist/io/media/convert.mjs.map +1 -0
  40. package/dist/{core/recorder → io/media}/download.mjs +8 -7
  41. package/dist/io/media/download.mjs.map +1 -0
  42. package/dist/{core/recorder/ffmpeg-utils.mjs → io/media/ffmpeg.mjs} +45 -5
  43. package/dist/io/media/ffmpeg.mjs.map +1 -0
  44. package/dist/{core/recorder → io/media}/normalize.mjs +1 -1
  45. package/dist/io/media/normalize.mjs.map +1 -0
  46. package/dist/io/tiktok/client.mjs +77 -0
  47. package/dist/io/tiktok/client.mjs.map +1 -0
  48. package/dist/io/tiktok/discovery/chain.mjs +117 -0
  49. package/dist/io/tiktok/discovery/chain.mjs.map +1 -0
  50. package/dist/io/tiktok/discovery/fallback.mjs +64 -0
  51. package/dist/io/tiktok/discovery/fallback.mjs.map +1 -0
  52. package/dist/io/tiktok/discovery/page.mjs +102 -0
  53. package/dist/io/tiktok/discovery/page.mjs.map +1 -0
  54. package/dist/io/tiktok/page.mjs +34 -0
  55. package/dist/io/tiktok/page.mjs.map +1 -0
  56. package/dist/io/tiktok/parse.mjs +110 -0
  57. package/dist/io/tiktok/parse.mjs.map +1 -0
  58. package/dist/io/tiktok/webcast.mjs +132 -0
  59. package/dist/io/tiktok/webcast.mjs.map +1 -0
  60. package/package.json +5 -26
  61. package/src/app/abort.ts +18 -0
  62. package/src/app/config.ts +178 -0
  63. package/src/app/events.ts +46 -0
  64. package/src/app/job.ts +228 -0
  65. package/src/app/logger.ts +133 -0
  66. package/src/app/loop.ts +84 -0
  67. package/src/app/recorder.ts +288 -0
  68. package/src/core/errors.ts +24 -0
  69. package/src/core/format.ts +50 -0
  70. package/src/core/log.ts +13 -0
  71. package/src/core/plan.ts +49 -0
  72. package/src/core/verdict.ts +79 -0
  73. package/src/index.ts +38 -0
  74. package/src/io/http.ts +167 -0
  75. package/src/io/media/convert.ts +69 -0
  76. package/src/io/media/download.ts +120 -0
  77. package/src/io/media/ffmpeg.ts +201 -0
  78. package/src/io/media/normalize.ts +64 -0
  79. package/src/io/tiktok/client.ts +114 -0
  80. package/src/io/tiktok/discovery/chain.ts +146 -0
  81. package/src/io/tiktok/discovery/fallback.ts +88 -0
  82. package/src/io/tiktok/discovery/page.ts +142 -0
  83. package/src/io/tiktok/page.ts +42 -0
  84. package/src/io/tiktok/parse.ts +171 -0
  85. package/src/io/tiktok/webcast.ts +162 -0
  86. package/dist/bin.d.mts +0 -1
  87. package/dist/bin.mjs +0 -215
  88. package/dist/bin.mjs.map +0 -1
  89. package/dist/core/api/client.mjs.map +0 -1
  90. package/dist/core/api/rate-limiter.mjs +0 -29
  91. package/dist/core/api/rate-limiter.mjs.map +0 -1
  92. package/dist/core/api/tiktok.mjs +0 -394
  93. package/dist/core/api/tiktok.mjs.map +0 -1
  94. package/dist/core/monitor.mjs.map +0 -1
  95. package/dist/core/recorder/convert.mjs +0 -55
  96. package/dist/core/recorder/convert.mjs.map +0 -1
  97. package/dist/core/recorder/download.mjs.map +0 -1
  98. package/dist/core/recorder/ffmpeg-utils.mjs.map +0 -1
  99. package/dist/core/recorder/index.d.mts +0 -7
  100. package/dist/core/recorder/index.d.mts.map +0 -1
  101. package/dist/core/recorder/index.mjs +0 -205
  102. package/dist/core/recorder/index.mjs.map +0 -1
  103. package/dist/core/recorder/normalize.mjs.map +0 -1
  104. package/dist/core/recorder/post-processing.mjs +0 -154
  105. package/dist/core/recorder/post-processing.mjs.map +0 -1
  106. package/dist/core/recorder/recorder-events.mjs +0 -34
  107. package/dist/core/recorder/recorder-events.mjs.map +0 -1
  108. package/dist/core/recorder/recorder-state.mjs +0 -38
  109. package/dist/core/recorder/recorder-state.mjs.map +0 -1
  110. package/dist/core/recorder/stream.mjs +0 -25
  111. package/dist/core/recorder/stream.mjs.map +0 -1
  112. package/dist/lib/browser-cookies.mjs +0 -113
  113. package/dist/lib/browser-cookies.mjs.map +0 -1
  114. package/dist/lib/cli.mjs +0 -137
  115. package/dist/lib/cli.mjs.map +0 -1
  116. package/dist/lib/config.d.mts.map +0 -1
  117. package/dist/lib/config.mjs +0 -47
  118. package/dist/lib/config.mjs.map +0 -1
  119. package/dist/lib/logger.mjs.map +0 -1
  120. package/dist/lib/tmux.mjs +0 -84
  121. package/dist/lib/tmux.mjs.map +0 -1
  122. package/dist/lib/ui.mjs +0 -136
  123. package/dist/lib/ui.mjs.map +0 -1
  124. package/dist/lib/utils.mjs.map +0 -1
  125. package/dist/package.mjs +0 -6
  126. package/dist/package.mjs.map +0 -1
package/README.md CHANGED
@@ -1,430 +1,126 @@
1
- <div align="center">
1
+ # @zfadhli/tokrec
2
2
 
3
- # tokrec
3
+ Library half of [tokrec](https://github.com/zfadhli/tokrec) — polls a TikTok user's live status, records the stream while it is live, and returns playable MP4 files.
4
4
 
5
- **Minimal TikTok live stream recorder — Bun + TypeScript**
6
-
7
- [![CI Status](https://img.shields.io/github/actions/workflow/status/zfadhli/tokrec/ci.yml?style=flat-square&label=CI)](https://github.com/zfadhli/tokrec/actions/workflows/ci.yml)
8
5
  [![npm version](https://img.shields.io/npm/v/@zfadhli/tokrec?style=flat-square)](https://www.npmjs.com/package/@zfadhli/tokrec)
9
- [![npm downloads](https://img.shields.io/npm/dw/@zfadhli/tokrec?style=flat-square)](https://www.npmjs.com/package/@zfadhli/tokrec)
10
- [![Bun](https://img.shields.io/badge/Bun-%3E%3D1.2-14151a?style=flat-square&logo=bun)](https://bun.sh)
11
- [![License](https://img.shields.io/npm/l/@zfadhli/tokrec?style=flat-square)](LICENSE)
12
-
13
- [Features](#features) · [Installation](#installation) · [Quick start](#quick-start) · [CLI options](#cli-options) · [Background recording](#background-recording-tmux) · [Authentication](#authentication) · [How it works](#how-it-works) · [Library API](#library-api) · [FAQ](#faq)
14
-
15
- </div>
16
-
17
- `tokrec` polls a TikTok user's live status, detects when they go live, downloads the stream, and post-processes it with FFmpeg into playable MP4 segments. Available as both a CLI tool and a TypeScript library.
18
-
19
- > [!NOTE]
20
- > This is **not** an all-in-one TikTok downloader. It focuses on **live stream recording** with automatic reconnection, graceful shutdown, and clean terminal output. For downloading uploaded videos, see other tools.
21
-
22
- ---
23
-
24
- ## Features
25
-
26
- - **Automatic polling** — checks every N minutes (default 3) and starts recording as soon as the user goes live
27
- - **Stream reconnection** — TikTok stream segments are short-lived (30-60s). When one ends, `tokrec` transparently fetches a fresh URL and continues writing to the same file
28
- - **Duration-limited recording** — record for a fixed time (`-d`) and auto-exit, or record indefinitely (default)
29
- - **Time-aligned MP4 segments** — post-recording, optionally split the stream into configurable-length MP4 segments, each independently playable
30
- - **Audio normalization** — EBU R128 two-pass loudness normalization via [peaknorm](https://github.com/zfadhli/peaknorm)
31
- - **Graceful shutdown** — responds to `SIGINT`/`SIGTERM`/`SIGHUP`. Aborts the download, flushes buffered data, converts to MP4, and exits cleanly
32
- - **Firefox cookie auto-detection** — reads `sessionid_ss` from Firefox's SQLite cookie store automatically (no config file needed)
33
- - **Live progress display** — animated spinner with real-time byte count, elapsed time, and download speed
34
- - **Typed event system** — 15 events with typed payloads for lifecycle tracking
35
- - **Proxy support** — route traffic through an HTTP proxy for regional access
36
- - **FLV + HLS support** — automatically detects and handles both stream formats
37
- - **Background recording** — tmux integration keeps recordings alive after terminal close, with per-user session management
38
- - **Multi-user** — record multiple users simultaneously, each in its own tmux session
39
- - **71+ passing tests** — unit tests with real TikTok HTML/API fixtures
40
-
41
- ---
42
-
43
- ## Requirements
44
-
45
- - **FFmpeg** — must be on your `$PATH` for MP4 conversion and segmenting
46
- - Linux: `apt install ffmpeg`
47
- - macOS: `brew install ffmpeg`
48
- - Windows: `choco install ffmpeg`
49
- - **Bun** (optional, for development) — Node.js 20+ can run the built package
50
-
51
- ---
52
-
53
- ## Installation
54
-
55
- ### CLI tool (global)
56
-
57
- ```bash
58
- npm install -g @zfadhli/tokrec
59
- ```
60
-
61
- Then run:
6
+ [![CI](https://img.shields.io/github/actions/workflow/status/zfadhli/tokrec/ci.yml?style=flat-square&label=CI)](https://github.com/zfadhli/tokrec/actions/workflows/ci.yml)
7
+ [![License](https://img.shields.io/npm/l/@zfadhli/tokrec?style=flat-square)](https://github.com/zfadhli/tokrec/blob/main/LICENSE)
62
8
 
63
- ```bash
64
- tokrec -u username
65
- ```
9
+ Want the ready-made command-line recorder instead? Use
10
+ [`@zfadhli/tokrec-cli`](https://www.npmjs.com/package/@zfadhli/tokrec-cli).
66
11
 
67
- ### One-off use (npx)
68
-
69
- ```bash
70
- npx @zfadhli/tokrec -u username
71
- ```
72
-
73
- ### As a library
12
+ ## Install
74
13
 
75
14
  ```bash
15
+ bun add @zfadhli/tokrec
16
+ # or
76
17
  npm install @zfadhli/tokrec
77
18
  ```
78
19
 
79
- ```ts
80
- import { createRecorder } from "@zfadhli/tokrec";
81
-
82
- const recorder = createRecorder({ user: "username" });
83
- recorder.on("recording:end", (info) => console.log("Done:", info.file));
84
- await recorder.start();
85
- ```
86
-
87
- > [!TIP]
88
- > When using as a library, logs print to the console by default. To suppress console output (e.g., if you run your own UI), set `logConsole: false` in the config.
89
-
90
- ---
20
+ Built for Bun ≥ 1.2. Nothing in the library is Bun-only — `Bun.which()` is used
21
+ when it exists and falls back to a `PATH` search — so the published `dist/`
22
+ also runs under Node ≥ 20. **FFmpeg must be on `PATH`** for MP4 conversion,
23
+ segmenting, and normalization.
91
24
 
92
25
  ## Quick start
93
26
 
94
- ```bash
95
- # Record a live stream (Ctrl+C to stop)
96
- tokrec -u username
97
-
98
- # Record for 10 minutes, then exit
99
- tokrec -u username -d 10
100
-
101
- # Record for 10 minutes, split into 5-minute segments
102
- tokrec -u username -d 10 -s 5
103
-
104
- # Use a proxy and custom output directory
105
- tokrec -u username -o ./videos -p http://127.0.0.1:8080
106
-
107
- # Enable audio normalization
108
- tokrec -u username --normalize
109
- ```
110
-
111
- ### Background recording (tmux)
112
-
113
- ```bash
114
- # Start in background — survives terminal close
115
- tokrec start -u username
116
-
117
- # Record multiple users simultaneously
118
- tokrec start -u user1
119
- tokrec start -u user2
120
-
121
- # Check active sessions
122
- tokrec status
123
-
124
- # Reattach to see live output
125
- tmux attach -t tokrec-user1
126
-
127
- # Detach without killing (Ctrl+B, D)
128
-
129
- # Stop a specific user
130
- tokrec stop -u user1
131
-
132
- # Stop all sessions
133
- tokrec stop
134
- ```
135
-
136
- > [!TIP]
137
- > Both `-d` and `-s` accept values in **minutes** (the tool converts them internally).
138
-
139
- ---
140
-
141
- ## CLI options
142
-
143
- | Option | Shorthand | Default | Description |
144
- | ----------------------------- | --------- | ---------------- | ------------------------------------------------------------------------------------------ |
145
- | `--user <name>` | `-u` | _(required)_ | TikTok username (with or without `@`) |
146
- | `--output <dir>` | `-o` | `./recordings` | Output directory |
147
- | `--interval <minutes>` | `-i` | `3` | Polling interval (min 1) |
148
- | `--duration <minutes>` | `-d` | unlimited | Max recording duration. Implies one-shot (exit after recording) |
149
- | `--segment-minutes <minutes>` | `-s` | disabled | Split recording into MP4 segments of this length |
150
- | `--proxy <url>` | `-p` | none | HTTP proxy URL |
151
- | `--cookies <path>` | `-c` | `./cookies.json` | Path to cookies JSON file |
152
- | `--log-level <level>` | `-l` | `info` | One of: `debug`, `info`, `warn`, `error` |
153
- | `--normalize` | _(none)_ | on | Enable EBU R128 audio normalization (enabled by default, pass `--no-normalize` to disable) |
154
- | `--normalize-loudness <num>` | _(none)_ | `-14` | Target loudness in LUFS |
155
- | `--normalize-codec <name>` | _(none)_ | `libopus` | Audio codec for normalized output (peaknorm default) |
156
- | `--normalize-bitrate <str>` | _(none)_ | `96k` | Audio bitrate for normalized output (peaknorm default) |
157
- | `--rate <n>` | _(none)_ | `5` | Max API requests/sec (0 = unlimited). Prevents WAF triggering |
158
- | `--debug` | _(none)_ | off | Show API debug logging on stderr |
159
-
160
- ### Subcommands
161
-
162
- | Command | Description | Example |
163
- | -------- | ---------------------------------------- | ------------------------------------------- |
164
- | `start` | Start recorder in a tmux session | `tokrec start -u username` |
165
- | `stop` | Stop tmux session(s) | `tokrec stop -u username` (or `tokrec stop` to stop all) |
166
- | `status` | Show active tmux sessions | `tokrec status` |
167
- | `setup` | Check cookie status or create cookies.json | `tokrec setup` |
168
-
169
- ---
170
-
171
- ## Authentication
172
-
173
- TikTok's Slardar WAF blocks unauthenticated requests. You must provide a valid `sessionid_ss` cookie to bypass it.
174
-
175
- ### Quick setup
176
-
177
- ```bash
178
- tokrec setup
179
- ```
180
-
181
- This checks for Firefox cookies (auto-detected) or `cookies.json`. If neither is found, it prompts you to enter your cookie value interactively.
182
-
183
- ### Option 1: Firefox (auto-detected)
184
-
185
- If you're logged into TikTok in Firefox, `tokrec` automatically reads the cookie from Firefox's SQLite store. No config file needed.
186
-
187
- ```bash
188
- tokrec -u username
189
- ```
190
-
191
- You will see:
192
-
193
- ```
194
- tokrec v0.11.0
195
- ℹ Firefox cookies loaded (30 cookies)
196
- ```
197
-
198
- ### Option 2: cookies.json
199
-
200
- 1. Open TikTok in any browser and log in.
201
- 2. Open DevTools → **Application** → **Cookies** → `www.tiktok.com`.
202
- 3. Copy the value of `sessionid_ss` and create a `cookies.json` file:
203
-
204
- ```json
205
- {
206
- "sessionid_ss": "6c74daa215a8f34eeddf5162ca091668"
207
- }
208
- ```
209
-
210
- You can also include the `tt-target-idc` cookie (optional, for regional routing):
211
-
212
- ```json
213
- {
214
- "sessionid_ss": "6c74daa215a8f34eeddf5162ca091668",
215
- "tt-target-idc": "useast2a"
216
- }
217
- ```
218
-
219
- 4. Place `cookies.json` in the working directory (auto-detected) or pass `--cookies ./path/to/cookies.json`.
220
-
221
- > [!CAUTION]
222
- > Without valid cookies, the tool will report every user as **offline** because the WAF challenge page won't contain live-stream JSON data.
223
-
224
- ---
225
-
226
- ## How it works
227
-
228
- ### Detection flow
229
-
230
- ```
231
- fetchLiveInfo(user)
232
- ├─ tryFetchPage (www.tiktok.com → m.tiktok.com)
233
- │ └─ WAF blocks both → API fallback
234
- ├─ /api-live/user/room/ (bypasses WAF)
235
- │ └─ Returns roomId + status (2=live, 4=offline)
236
- └─ If live → /webcast/room/info/ for stream URL
237
-
238
- isRoomAlive(roomId) [during recording]
239
- ├─ Cache hit → return cached.isLive
240
- └─ Cache miss → /webcast/room/check_alive/ → boolean (~100 bytes)
241
-
242
- getNextUrl() [during recording]
243
- ├─ isRoomAlive(roomId) → if dead, stop
244
- ├─ Invalidate cache
245
- └─ getLiveUrl(roomId) → fresh stream URL
246
- ```
247
-
248
- ### Pipeline
249
-
250
- ```
251
- ┌────────────┐ ┌──────────────┐ ┌──────────────┐ ┌─────────────────┐
252
- │ Poll tick │ → │ Detect live │ → │ Download TS │ → │ FFmpeg post │
253
- │ (every Nm) │ │ 3-tier │ │ (auto-reconn.)│ │ (convert/ │
254
- │ │ │ fallback │ │ │ │ segment) │
255
- └────────────┘ └──────────────┘ └──────────────┘ └─────────────────┘
256
- ```
257
-
258
- 1. **Poll tick** — every N minutes, the monitor calls the check function.
259
- 2. **Live detection** — uses a three-tier fallback:
260
- - Fast-path from `SIGI_STATE` on the `/live` page (~95% of cases)
261
- - Brute-force recursive search of the entire JSON (survives key name changes)
262
- - Webcast API fallback (`/webcast/room/info/`)
263
- 3. **Download** — the stream is downloaded via FFmpeg stdout pipe to a `.ts` file. TikTok's short-lived URLs are handled transparently: when a URL expires, `getNextUrl()` fetches a fresh one. Duration limit is enforced **inside** FFmpeg via the `-t` flag for precise cutoff.
264
- 4. **Post-processing** — the `.ts` file is remuxed to MP4 via `ffmpeg -c copy` (no re-encode, fast). If segmenting is enabled, FFmpeg's segment muxer splits into `_partN.mp4` files. Audio normalization runs last if enabled.
265
-
266
- ### Output structure
267
-
268
- ```
269
- ./recordings/
270
- username=20260614_143000.ts # raw download (deleted after conversion)
271
- username=20260614_143000.mp4 # remuxed MP4 (no segmenting)
272
- username=20260614_143000_part1.mp4 # segment 1 (with -s)
273
- username=20260614_143000_part2.mp4 # segment 2
274
- ...
275
- ```
276
-
277
- File naming: `{username}={YYYYMMDD}_{HHMMSS}[_partN].mp4`
278
-
279
- ### Event system
280
-
281
- The recorder emits typed events that you can subscribe to via `recorder.on()`:
282
-
283
- | Event | Payload | When |
284
- | -------------------- | --------------------------- | --------------------------------- |
285
- | `checking` | `{ user }` | Before each poll tick |
286
- | `tick` | `{ user, isLive, roomId? }` | After checking live status |
287
- | `recording:start` | `{ user, file }` | When recording begins |
288
- | `download:progress` | `{ bytes, elapsed, speed }` | During download (~1s intervals) |
289
- | `download:end` | `{ file, duration, size }` | When download completes |
290
- | `recording:end` | `{ file, duration, size }` | Per segment/file after conversion |
291
- | `segmenting:start` | `{ input, outputPattern }` | Before FFmpeg segmenting |
292
- | `segmenting:end` | `{ segments }` | After segmenting |
293
- | `converting:start` | `{ input }` | Before simple conversion |
294
- | `converted` | `{ input, output }` | After each converted file |
295
- | `normalize:start` | `{ file }` | Before audio normalization |
296
- | `normalize:progress` | `{ file, percent, phase }` | During normalization |
297
- | `normalize:end` | `{ input, output }` | After normalization |
298
- | `normalize:error` | `{ input, error }` | On normalization error |
299
- | `error` | `err: TikTokError` | On non-fatal errors |
300
-
301
- ---
302
-
303
- ## Library API
304
-
305
- ### `createRecorder(config)`
306
-
307
27
  ```ts
308
- import { createRecorder } from "@zfadhli/tokrec";
309
- import type { RecorderConfig, RecorderController, RecorderEvent } from "@zfadhli/tokrec";
310
-
311
- const recorder: RecorderController = createRecorder({
312
- user: "username", // required
313
- outputDir: "./videos", // default: './recordings'
314
- interval: 3, // polling interval in minutes
315
- duration: 600, // max recording duration in seconds
316
- segmentMinutes: 20, // segment length in minutes
317
- proxy: "http://127.0.0.1:8080",
318
- cookies: { sessionid_ss: "..." },
319
- logLevel: "info",
320
- logConsole: true,
321
- });
322
- ```
323
-
324
- ### `RecorderController`
28
+ import { createRecorder } from "@zfadhli/tokrec"
29
+
30
+ const recorder = createRecorder({
31
+ user: "somebody",
32
+ outputDir: "./recordings",
33
+ interval: 3, // minutes between live-status polls
34
+ })
35
+
36
+ recorder.on("verdict", (v) => console.log("status:", v.reason))
37
+ recorder.on("progress", (p) => {
38
+ if (p.kind === "download") console.log(`${p.percent.toFixed(1)}%`)
39
+ })
40
+ recorder.on("record", (e) => {
41
+ if (e.phase === "end") console.log("saved:", e.file)
42
+ })
43
+ recorder.on("failure", (err) => console.error(err.code, err.message))
44
+
45
+ await recorder.start() // polls until offline or stop()
46
+ ```
47
+
48
+ `start()` resolves when the run ends; call `stop()` from a signal handler to shut
49
+ down gracefully — the download is aborted, buffered data is flushed, and the
50
+ partial recording is still remuxed to `.mp4` before the promise settles.
51
+
52
+ ## Configuration
53
+
54
+ | Field | Type | Default | Meaning |
55
+ | --- | --- | --- | --- |
56
+ | `user` | `string` | — | TikTok username (required) |
57
+ | `outputDir` | `string` | `./recordings` | Destination directory |
58
+ | `interval` | `number` | `3` | Minutes between live-status polls |
59
+ | `duration` | `number` | `0` (unlimited) | Max recording length in **seconds** |
60
+ | `proxy` | `string` | — | HTTP proxy, e.g. `http://127.0.0.1:8080` |
61
+ | `cookiesPath` | `string` | `./cookies.json` | Path to a cookies file |
62
+ | `cookies` | `CookieAuth` | — | Cookie auth passed as an object instead of a file |
63
+ | `logLevel` | `"debug" \| "info" \| "warn" \| "error"` | `info` | Console verbosity |
64
+ | `logConsole` | `boolean` | `true` | Set `false` when you render your own output |
65
+ | `segmentMinutes` | `number` | `0` (off) | Split the recording into N-minute segments |
66
+ | `normalizeAudio` | `boolean` | `false` | EBU R128 loudness normalization via [peaknorm](https://github.com/zfadhli/peaknorm) |
67
+ | `normalizeLoudness` | `number` | `-14` | Target loudness in LUFS |
68
+ | `normalizeCodec` | `string` | `libopus` | Audio codec for normalized output |
69
+ | `normalizeBitrate` | `string` | `96k` | Audio bitrate for normalized output |
70
+ | `ratePerSecond` | `number` | `5` | Request cap; `0` disables the limiter |
71
+ | `debug` | `boolean` | `false` | API debug logging on stderr |
72
+
73
+ > The CLI accepts `--duration` in minutes; the library takes seconds.
74
+
75
+ ## Events
76
+
77
+ Four typed channels:
78
+
79
+ | Event | Payload | Fires when |
80
+ | --- | --- | --- |
81
+ | `verdict` | `Verdict` — `reason`, `stream`, `evidence` | Every status poll resolves |
82
+ | `record` | `RecordEvent` | Recording lifecycle: `start`, `download`, `segment`, `convert`, `normalize`, `end` |
83
+ | `progress` | `ProgressEvent` — `{ kind: "download" }` or `{ kind: "normalize" }` | Bytes/percent updates |
84
+ | `failure` | `TikTokError` | A recoverable or fatal error |
85
+
86
+ `Verdict["reason"]` is one of `live`, `offline`, `restricted`, `no-stream-url`,
87
+ `no-room-id`, `error`, and carries the evidence sources that produced it.
88
+
89
+ ## Controller
325
90
 
326
91
  ```ts
327
92
  interface RecorderController {
328
- start(): Promise<void>;
329
- stop(): Promise<void>;
330
- getStatus(): RecorderStatus;
331
- on<E extends RecorderEvent>(event: E, handler: RecorderEventHandler[E]): void;
332
- off<E extends RecorderEvent>(event: E, handler: RecorderEventHandler[E]): void;
93
+ start(): Promise<void>
94
+ stop(): Promise<void> // graceful: joins the active recording first
95
+ getStatus(): RecorderStatus
96
+ on(event, handler): void
97
+ off(event, handler): void
333
98
  }
334
99
  ```
335
100
 
336
- ### `RecorderStatus`
101
+ `RecorderStatus` exposes `state`, `user`, `currentFile`, `sessionDuration`,
102
+ `lastPollTime`, and `lastError`.
337
103
 
338
- ```ts
339
- interface RecorderStatus {
340
- state: "idle" | "polling" | "recording" | "converting" | "stopped";
341
- user: string;
342
- currentFile?: string;
343
- sessionDuration?: number;
344
- lastPollTime?: string;
345
- lastError?: string;
346
- }
347
- ```
348
-
349
- ### Example: build a notification bot
350
-
351
- ```ts
352
- import { createRecorder } from "@zfadhli/tokrec";
353
-
354
- const recorder = createRecorder({ user: "username" });
355
-
356
- recorder.on("checking", ({ user }) => console.log(`Checking @${user}...`));
357
- recorder.on("tick", ({ isLive, roomId, user }) => {
358
- if (isLive) {
359
- console.log(`@${user} is LIVE! (room: ${roomId})`);
360
- // Send a Discord/Telegram notification here
361
- }
362
- });
363
- recorder.on("recording:end", ({ file, duration }) => {
364
- console.log(`Recorded ${file} (${Math.round(duration)}s)`);
365
- });
366
- recorder.on("error", (err) => console.error(`[${err.kind}] ${err.message}`));
367
-
368
- await recorder.start();
369
- ```
104
+ ## Exports
370
105
 
371
- ---
106
+ | Export | Kind | Purpose |
107
+ | --- | --- | --- |
108
+ | `createRecorder(config)` | value | Build a `RecorderController` |
109
+ | `TikTokError` | value | Typed error class |
110
+ | `isLiveReason(reason)` | value | Narrow a verdict reason |
111
+ | `bytesToHuman`, `formatDuration`, `relativeTime`, `sanitizeUser` | value | Formatting helpers used by the CLI |
112
+ | `RecorderConfig`, `RecorderController`, `RecorderStatus`, `RecorderEvent`, `RecorderEventHandler`, `RecordEvent`, `ProgressEvent`, `RecordedFile`, `LiveReason`, `Verdict`, `Evidence`, `EvidenceOutcome`, `RoomRef`, `StreamRef`, `SourceId`, `Phase`, `LogLevel`, `TikTokErrorCode` | type | Public types (18) |
372
113
 
373
- ## FAQ
374
-
375
- ### Why does the tool say "offline" for every user?
376
-
377
- You likely don't have a valid `sessionid_ss` cookie. See the [Authentication](#authentication) section.
378
-
379
- ### Why does the download stop after 30 seconds?
380
-
381
- TikTok serves live streams as short FLV segments (~30-60s). `tokrec` handles reconnection transparently — this is expected behavior and the download continues with a fresh URL.
382
-
383
- ### Can I record multiple users at once?
384
-
385
- Yes. Use tmux background sessions:
386
-
387
- ```bash
388
- tokrec start -u user1
389
- tokrec start -u user2
390
- tokrec status # shows all active sessions
391
- ```
392
-
393
- Each user gets an independent tmux session (`tokrec-{user}`), so they run in parallel without interfering.
394
-
395
- ### Can I use this with Node.js instead of Bun?
396
-
397
- Yes. The package is built to ESM for Node.js 20+. Bun is optional for development.
398
-
399
- ### Where are the log files?
400
-
401
- `tiktok-recorder.log` in the working directory, with rotating backups. Console output is also available via `--log-level`.
402
-
403
- ### Does it work if the user goes live while I'm not running it?
404
-
405
- No. `tokrec` only polls while it's running. Use tmux background sessions for always-on monitoring:
406
-
407
- ```bash
408
- tokrec start -u username # runs in tmux, survives terminal close
409
- tmux attach -t tokrec-username # reattach to view output
410
- ```
411
-
412
- ---
413
-
414
- ## Troubleshooting
415
-
416
- ### FFmpeg not found
417
-
418
- Install FFmpeg and ensure it is on your `$PATH`:
419
-
420
- ```bash
421
- which ffmpeg # should print a path
422
- ```
114
+ Error codes: `config-error`, `ffmpeg-not-found`, `ffmpeg-error`, `aborted`,
115
+ `unknown`.
423
116
 
424
- ### "WAF blocked" errors
117
+ ## Output
425
118
 
426
- Your `sessionid_ss` cookie has expired or is invalid. Generate a fresh one from your browser's DevTools.
119
+ Recordings are named `<user>=YYYYMMDD_HHMMSS.mp4`; segmenting produces
120
+ `<name>_partN.mp4`.
427
121
 
428
- ### The process doesn't exit after a `-d` recording
122
+ ## Links
429
123
 
430
- Make sure you're using v0.7.1 or later. Earlier versions continued polling instead of exiting.
124
+ - [CLI package](https://www.npmjs.com/package/@zfadhli/tokrec-cli)
125
+ - [Repository](https://github.com/zfadhli/tokrec) · [Changelog](https://github.com/zfadhli/tokrec/blob/main/CHANGELOG.md)
126
+ - [MIT License](https://github.com/zfadhli/tokrec/blob/main/LICENSE)
@@ -0,0 +1,14 @@
1
+ //#region src/app/abort.ts
2
+ function abortScope(parent) {
3
+ const controller = new AbortController();
4
+ if (parent) if (parent.aborted) controller.abort(parent.reason);
5
+ else parent.addEventListener("abort", () => controller.abort(parent.reason), { once: true });
6
+ return {
7
+ signal: controller.signal,
8
+ close: (reason) => controller.abort(reason)
9
+ };
10
+ }
11
+ //#endregion
12
+ export { abortScope };
13
+
14
+ //# sourceMappingURL=abort.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"abort.mjs","names":[],"sources":["../../src/app/abort.ts"],"sourcesContent":["/**\n * One cancellation scope per recorder run. Everything that can block accepts\n * `scope.signal`; `close()` aborts in-flight work immediately.\n */\n\nexport interface AbortScope {\n readonly signal: AbortSignal\n close(reason?: string): void\n}\n\nexport function abortScope(parent?: AbortSignal): AbortScope {\n const controller = new AbortController()\n if (parent) {\n if (parent.aborted) controller.abort(parent.reason)\n else parent.addEventListener(\"abort\", () => controller.abort(parent.reason), { once: true })\n }\n return { signal: controller.signal, close: (reason) => controller.abort(reason) }\n}\n"],"mappings":";AAUA,SAAgB,WAAW,QAAkC;CAC3D,MAAM,aAAa,IAAI,gBAAgB;CACvC,IAAI,QACF,IAAI,OAAO,SAAS,WAAW,MAAM,OAAO,MAAM;MAC7C,OAAO,iBAAiB,eAAe,WAAW,MAAM,OAAO,MAAM,GAAG,EAAE,MAAM,KAAK,CAAC;CAE7F,OAAO;EAAE,QAAQ,WAAW;EAAQ,QAAQ,WAAW,WAAW,MAAM,MAAM;CAAE;AAClF"}