@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.
- package/README.md +99 -403
- package/dist/app/abort.mjs +14 -0
- package/dist/app/abort.mjs.map +1 -0
- package/dist/{lib → app}/config.d.mts +73 -71
- package/dist/app/config.d.mts.map +1 -0
- package/dist/app/config.mjs +26 -0
- package/dist/app/config.mjs.map +1 -0
- package/dist/app/events.mjs +36 -0
- package/dist/app/events.mjs.map +1 -0
- package/dist/app/job.mjs +190 -0
- package/dist/app/job.mjs.map +1 -0
- package/dist/{lib → app}/logger.mjs +1 -1
- package/dist/app/logger.mjs.map +1 -0
- package/dist/{core/monitor.mjs → app/loop.mjs} +19 -9
- package/dist/app/loop.mjs.map +1 -0
- package/dist/app/recorder.d.mts +7 -0
- package/dist/app/recorder.d.mts.map +1 -0
- package/dist/app/recorder.mjs +233 -0
- package/dist/app/recorder.mjs.map +1 -0
- package/dist/core/errors.d.mts +16 -0
- package/dist/core/errors.d.mts.map +1 -0
- package/dist/core/errors.mjs +15 -0
- package/dist/core/errors.mjs.map +1 -0
- package/dist/core/format.d.mts +12 -0
- package/dist/core/format.d.mts.map +1 -0
- package/dist/{lib/utils.mjs → core/format.mjs} +6 -41
- package/dist/core/format.mjs.map +1 -0
- package/dist/core/plan.mjs +52 -0
- package/dist/core/plan.mjs.map +1 -0
- package/dist/core/verdict.d.mts +44 -0
- package/dist/core/verdict.d.mts.map +1 -0
- package/dist/core/verdict.mjs +47 -0
- package/dist/core/verdict.mjs.map +1 -0
- package/dist/index.d.mts +6 -3
- package/dist/index.mjs +5 -2
- package/dist/{core/api/client.mjs → io/http.mjs} +38 -10
- package/dist/io/http.mjs.map +1 -0
- package/dist/io/media/convert.mjs +60 -0
- package/dist/io/media/convert.mjs.map +1 -0
- package/dist/{core/recorder → io/media}/download.mjs +8 -7
- package/dist/io/media/download.mjs.map +1 -0
- package/dist/{core/recorder/ffmpeg-utils.mjs → io/media/ffmpeg.mjs} +45 -5
- package/dist/io/media/ffmpeg.mjs.map +1 -0
- package/dist/{core/recorder → io/media}/normalize.mjs +1 -1
- package/dist/io/media/normalize.mjs.map +1 -0
- package/dist/io/tiktok/client.mjs +77 -0
- package/dist/io/tiktok/client.mjs.map +1 -0
- package/dist/io/tiktok/discovery/chain.mjs +117 -0
- package/dist/io/tiktok/discovery/chain.mjs.map +1 -0
- package/dist/io/tiktok/discovery/fallback.mjs +64 -0
- package/dist/io/tiktok/discovery/fallback.mjs.map +1 -0
- package/dist/io/tiktok/discovery/page.mjs +102 -0
- package/dist/io/tiktok/discovery/page.mjs.map +1 -0
- package/dist/io/tiktok/page.mjs +34 -0
- package/dist/io/tiktok/page.mjs.map +1 -0
- package/dist/io/tiktok/parse.mjs +110 -0
- package/dist/io/tiktok/parse.mjs.map +1 -0
- package/dist/io/tiktok/webcast.mjs +132 -0
- package/dist/io/tiktok/webcast.mjs.map +1 -0
- package/package.json +5 -26
- package/src/app/abort.ts +18 -0
- package/src/app/config.ts +178 -0
- package/src/app/events.ts +46 -0
- package/src/app/job.ts +228 -0
- package/src/app/logger.ts +133 -0
- package/src/app/loop.ts +84 -0
- package/src/app/recorder.ts +288 -0
- package/src/core/errors.ts +24 -0
- package/src/core/format.ts +50 -0
- package/src/core/log.ts +13 -0
- package/src/core/plan.ts +49 -0
- package/src/core/verdict.ts +79 -0
- package/src/index.ts +38 -0
- package/src/io/http.ts +167 -0
- package/src/io/media/convert.ts +69 -0
- package/src/io/media/download.ts +120 -0
- package/src/io/media/ffmpeg.ts +201 -0
- package/src/io/media/normalize.ts +64 -0
- package/src/io/tiktok/client.ts +114 -0
- package/src/io/tiktok/discovery/chain.ts +146 -0
- package/src/io/tiktok/discovery/fallback.ts +88 -0
- package/src/io/tiktok/discovery/page.ts +142 -0
- package/src/io/tiktok/page.ts +42 -0
- package/src/io/tiktok/parse.ts +171 -0
- package/src/io/tiktok/webcast.ts +162 -0
- package/dist/bin.d.mts +0 -1
- package/dist/bin.mjs +0 -215
- package/dist/bin.mjs.map +0 -1
- package/dist/core/api/client.mjs.map +0 -1
- package/dist/core/api/rate-limiter.mjs +0 -29
- package/dist/core/api/rate-limiter.mjs.map +0 -1
- package/dist/core/api/tiktok.mjs +0 -394
- package/dist/core/api/tiktok.mjs.map +0 -1
- package/dist/core/monitor.mjs.map +0 -1
- package/dist/core/recorder/convert.mjs +0 -55
- package/dist/core/recorder/convert.mjs.map +0 -1
- package/dist/core/recorder/download.mjs.map +0 -1
- package/dist/core/recorder/ffmpeg-utils.mjs.map +0 -1
- package/dist/core/recorder/index.d.mts +0 -7
- package/dist/core/recorder/index.d.mts.map +0 -1
- package/dist/core/recorder/index.mjs +0 -205
- package/dist/core/recorder/index.mjs.map +0 -1
- package/dist/core/recorder/normalize.mjs.map +0 -1
- package/dist/core/recorder/post-processing.mjs +0 -154
- package/dist/core/recorder/post-processing.mjs.map +0 -1
- package/dist/core/recorder/recorder-events.mjs +0 -34
- package/dist/core/recorder/recorder-events.mjs.map +0 -1
- package/dist/core/recorder/recorder-state.mjs +0 -38
- package/dist/core/recorder/recorder-state.mjs.map +0 -1
- package/dist/core/recorder/stream.mjs +0 -25
- package/dist/core/recorder/stream.mjs.map +0 -1
- package/dist/lib/browser-cookies.mjs +0 -113
- package/dist/lib/browser-cookies.mjs.map +0 -1
- package/dist/lib/cli.mjs +0 -137
- package/dist/lib/cli.mjs.map +0 -1
- package/dist/lib/config.d.mts.map +0 -1
- package/dist/lib/config.mjs +0 -47
- package/dist/lib/config.mjs.map +0 -1
- package/dist/lib/logger.mjs.map +0 -1
- package/dist/lib/tmux.mjs +0 -84
- package/dist/lib/tmux.mjs.map +0 -1
- package/dist/lib/ui.mjs +0 -136
- package/dist/lib/ui.mjs.map +0 -1
- package/dist/lib/utils.mjs.map +0 -1
- package/dist/package.mjs +0 -6
- package/dist/package.mjs.map +0 -1
package/README.md
CHANGED
|
@@ -1,430 +1,126 @@
|
|
|
1
|
-
|
|
1
|
+
# @zfadhli/tokrec
|
|
2
2
|
|
|
3
|
-
|
|
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
|
-
[](https://github.com/zfadhli/tokrec/actions/workflows/ci.yml)
|
|
8
5
|
[](https://www.npmjs.com/package/@zfadhli/tokrec)
|
|
9
|
-
[](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
|
+
[](https://github.com/zfadhli/tokrec/actions/workflows/ci.yml)
|
|
7
|
+
[](https://github.com/zfadhli/tokrec/blob/main/LICENSE)
|
|
62
8
|
|
|
63
|
-
|
|
64
|
-
tokrec
|
|
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
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
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
|
|
332
|
-
off
|
|
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
|
-
|
|
101
|
+
`RecorderStatus` exposes `state`, `user`, `currentFile`, `sessionDuration`,
|
|
102
|
+
`lastPollTime`, and `lastError`.
|
|
337
103
|
|
|
338
|
-
|
|
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
|
-
|
|
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
|
-
|
|
117
|
+
## Output
|
|
425
118
|
|
|
426
|
-
|
|
119
|
+
Recordings are named `<user>=YYYYMMDD_HHMMSS.mp4`; segmenting produces
|
|
120
|
+
`<name>_partN.mp4`.
|
|
427
121
|
|
|
428
|
-
|
|
122
|
+
## Links
|
|
429
123
|
|
|
430
|
-
|
|
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"}
|