@zfadhli/tokrec 0.13.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/dist/app/abort.mjs +14 -0
  2. package/dist/app/abort.mjs.map +1 -0
  3. package/dist/{lib → app}/config.d.mts +73 -71
  4. package/dist/app/config.d.mts.map +1 -0
  5. package/dist/app/config.mjs +26 -0
  6. package/dist/app/config.mjs.map +1 -0
  7. package/dist/app/events.mjs +36 -0
  8. package/dist/app/events.mjs.map +1 -0
  9. package/dist/app/job.mjs +190 -0
  10. package/dist/app/job.mjs.map +1 -0
  11. package/dist/{lib → app}/logger.mjs +1 -1
  12. package/dist/app/logger.mjs.map +1 -0
  13. package/dist/{core/monitor.mjs → app/loop.mjs} +19 -9
  14. package/dist/app/loop.mjs.map +1 -0
  15. package/dist/app/recorder.d.mts +7 -0
  16. package/dist/app/recorder.d.mts.map +1 -0
  17. package/dist/app/recorder.mjs +233 -0
  18. package/dist/app/recorder.mjs.map +1 -0
  19. package/dist/core/errors.d.mts +16 -0
  20. package/dist/core/errors.d.mts.map +1 -0
  21. package/dist/core/errors.mjs +15 -0
  22. package/dist/core/errors.mjs.map +1 -0
  23. package/dist/core/format.d.mts +12 -0
  24. package/dist/core/format.d.mts.map +1 -0
  25. package/dist/{lib/utils.mjs → core/format.mjs} +6 -41
  26. package/dist/core/format.mjs.map +1 -0
  27. package/dist/core/plan.mjs +52 -0
  28. package/dist/core/plan.mjs.map +1 -0
  29. package/dist/core/verdict.d.mts +44 -0
  30. package/dist/core/verdict.d.mts.map +1 -0
  31. package/dist/core/verdict.mjs +47 -0
  32. package/dist/core/verdict.mjs.map +1 -0
  33. package/dist/index.d.mts +6 -3
  34. package/dist/index.mjs +5 -2
  35. package/dist/{core/api/client.mjs → io/http.mjs} +38 -10
  36. package/dist/io/http.mjs.map +1 -0
  37. package/dist/io/media/convert.mjs +60 -0
  38. package/dist/io/media/convert.mjs.map +1 -0
  39. package/dist/{core/recorder → io/media}/download.mjs +8 -7
  40. package/dist/io/media/download.mjs.map +1 -0
  41. package/dist/{core/recorder/ffmpeg-utils.mjs → io/media/ffmpeg.mjs} +45 -5
  42. package/dist/io/media/ffmpeg.mjs.map +1 -0
  43. package/dist/{core/recorder → io/media}/normalize.mjs +1 -1
  44. package/dist/io/media/normalize.mjs.map +1 -0
  45. package/dist/io/tiktok/client.mjs +77 -0
  46. package/dist/io/tiktok/client.mjs.map +1 -0
  47. package/dist/io/tiktok/discovery/chain.mjs +117 -0
  48. package/dist/io/tiktok/discovery/chain.mjs.map +1 -0
  49. package/dist/io/tiktok/discovery/fallback.mjs +64 -0
  50. package/dist/io/tiktok/discovery/fallback.mjs.map +1 -0
  51. package/dist/io/tiktok/discovery/page.mjs +102 -0
  52. package/dist/io/tiktok/discovery/page.mjs.map +1 -0
  53. package/dist/io/tiktok/page.mjs +34 -0
  54. package/dist/io/tiktok/page.mjs.map +1 -0
  55. package/dist/io/tiktok/parse.mjs +110 -0
  56. package/dist/io/tiktok/parse.mjs.map +1 -0
  57. package/dist/io/tiktok/webcast.mjs +132 -0
  58. package/dist/io/tiktok/webcast.mjs.map +1 -0
  59. package/package.json +5 -26
  60. package/src/app/abort.ts +18 -0
  61. package/src/app/config.ts +178 -0
  62. package/src/app/events.ts +46 -0
  63. package/src/app/job.ts +228 -0
  64. package/src/app/logger.ts +133 -0
  65. package/src/app/loop.ts +84 -0
  66. package/src/app/recorder.ts +288 -0
  67. package/src/core/errors.ts +24 -0
  68. package/src/core/format.ts +50 -0
  69. package/src/core/log.ts +13 -0
  70. package/src/core/plan.ts +49 -0
  71. package/src/core/verdict.ts +79 -0
  72. package/src/index.ts +38 -0
  73. package/src/io/http.ts +167 -0
  74. package/src/io/media/convert.ts +69 -0
  75. package/src/io/media/download.ts +120 -0
  76. package/src/io/media/ffmpeg.ts +201 -0
  77. package/src/io/media/normalize.ts +64 -0
  78. package/src/io/tiktok/client.ts +114 -0
  79. package/src/io/tiktok/discovery/chain.ts +146 -0
  80. package/src/io/tiktok/discovery/fallback.ts +88 -0
  81. package/src/io/tiktok/discovery/page.ts +142 -0
  82. package/src/io/tiktok/page.ts +42 -0
  83. package/src/io/tiktok/parse.ts +171 -0
  84. package/src/io/tiktok/webcast.ts +162 -0
  85. package/README.md +0 -430
  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 DELETED
@@ -1,430 +0,0 @@
1
- <div align="center">
2
-
3
- # tokrec
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
- [![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:
62
-
63
- ```bash
64
- tokrec -u username
65
- ```
66
-
67
- ### One-off use (npx)
68
-
69
- ```bash
70
- npx @zfadhli/tokrec -u username
71
- ```
72
-
73
- ### As a library
74
-
75
- ```bash
76
- npm install @zfadhli/tokrec
77
- ```
78
-
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
- ---
91
-
92
- ## Quick start
93
-
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
- ```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`
325
-
326
- ```ts
327
- 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;
333
- }
334
- ```
335
-
336
- ### `RecorderStatus`
337
-
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
- ```
370
-
371
- ---
372
-
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
- ```
423
-
424
- ### "WAF blocked" errors
425
-
426
- Your `sessionid_ss` cookie has expired or is invalid. Generate a fresh one from your browser's DevTools.
427
-
428
- ### The process doesn't exit after a `-d` recording
429
-
430
- Make sure you're using v0.7.1 or later. Earlier versions continued polling instead of exiting.
package/dist/bin.d.mts DELETED
@@ -1 +0,0 @@
1
- export { };
package/dist/bin.mjs DELETED
@@ -1,215 +0,0 @@
1
- #!/usr/bin/env bun
2
- import { TikTokError } from "./lib/config.mjs";
3
- import { bytesToHuman, relativeTime } from "./lib/utils.mjs";
4
- import { createRecorder } from "./core/recorder/index.mjs";
5
- import "./index.mjs";
6
- import { extractTikTokCookiesFromFirefox } from "./lib/browser-cookies.mjs";
7
- import { parseArgs } from "./lib/cli.mjs";
8
- import { tmuxStart, tmuxStatus, tmuxStop } from "./lib/tmux.mjs";
9
- import { createDisplay } from "./lib/ui.mjs";
10
- import { existsSync, readFileSync, writeFileSync } from "node:fs";
11
- import { join } from "node:path";
12
- import { color } from "@zfadhli/koko-cli";
13
- //#region src/bin.ts
14
- /**
15
- * Entry point — parses CLI args, creates the recorder, and handles signals.
16
- * Uses the terminal Display manager for beautiful console output.
17
- */
18
- async function loadCookies(cookiesPath) {
19
- const path = cookiesPath ?? join(process.cwd(), "cookies.json");
20
- if (!existsSync(path)) return;
21
- try {
22
- const content = readFileSync(path, "utf-8");
23
- const data = JSON.parse(content);
24
- if (data.sessionid_ss && data.sessionid_ss.length > 0) return data;
25
- return;
26
- } catch {
27
- return;
28
- }
29
- }
30
- async function main() {
31
- let result;
32
- try {
33
- result = parseArgs();
34
- } catch (err) {
35
- if (err instanceof TikTokError && err.kind === "config-error") {
36
- console.error(`${color.red("Error:")} ${err.message}`);
37
- process.exit(1);
38
- }
39
- throw err;
40
- }
41
- if (result.command === "start") {
42
- const userMatch = result.args.find((_, i, a) => a[i - 1] === "--user" || a[i - 1] === "-u");
43
- if (!userMatch) {
44
- console.error(`${color.red("Error:")} --user is required for "start" command`);
45
- process.exit(1);
46
- }
47
- const cmd = `tokrec ${result.args.join(" ")}`;
48
- try {
49
- await tmuxStart(userMatch, cmd);
50
- console.log(`${color.green("✓")} Session "tokrec-${userMatch}" started in background`);
51
- console.log(` tmux attach -t tokrec-${userMatch} — view live output`);
52
- console.log(` tokrec stop -u ${userMatch} — stop recording`);
53
- } catch (err) {
54
- console.error(`${color.red("Error:")} ${err instanceof Error ? err.message : String(err)}`);
55
- process.exit(1);
56
- }
57
- return;
58
- }
59
- if (result.command === "stop") {
60
- try {
61
- await tmuxStop(result.user);
62
- if (result.user) console.log(`${color.green("✓")} Session "tokrec-${result.user}" stopped`);
63
- else console.log(`${color.green("✓")} All tokrec sessions stopped`);
64
- } catch (err) {
65
- console.error(`${color.red("Error:")} ${err instanceof Error ? err.message : String(err)}`);
66
- process.exit(1);
67
- }
68
- return;
69
- }
70
- if (result.command === "status") {
71
- const sessions = await tmuxStatus(result.user);
72
- if (sessions.length === 0) console.log(`${color.gray("○")} No active session${result.user ? ` for "${result.user}"` : ""}`);
73
- else for (const s of sessions) {
74
- console.log(`${color.green("●")} tokrec-${s.user}`);
75
- console.log(` tmux attach -t ${s.session} — view live output`);
76
- }
77
- return;
78
- }
79
- if (result.command === "setup") {
80
- const firefoxCookies = extractTikTokCookiesFromFirefox();
81
- if (firefoxCookies) {
82
- const names = Object.keys(firefoxCookies);
83
- const detail = names.length > 1 ? ` (${names.length} cookies)` : "";
84
- console.log(`${color.green("✓")} Firefox cookies detected${detail}`);
85
- console.log(` No setup needed — tokrec auto-detects Firefox cookies.`);
86
- return;
87
- }
88
- const cookiesPath = join(process.cwd(), "cookies.json");
89
- if (existsSync(cookiesPath)) try {
90
- if (JSON.parse(readFileSync(cookiesPath, "utf-8")).sessionid_ss) {
91
- console.log(`${color.green("✓")} cookies.json found at ./cookies.json`);
92
- return;
93
- }
94
- } catch {}
95
- console.log(`${color.yellow("⚠")} No TikTok cookies found.`);
96
- console.log();
97
- console.log(`You need a valid sessionid_ss cookie to bypass TikTok's WAF.`);
98
- console.log(`Either log in to TikTok in Firefox (auto-detected), or enter manually:`);
99
- console.log();
100
- const sessionid = prompt(" sessionid_ss cookie value:");
101
- if (!sessionid || sessionid.trim().length === 0) {
102
- console.log(`${color.red("✗")} No cookie provided. Setup cancelled.`);
103
- return;
104
- }
105
- const cookies = { sessionid_ss: sessionid.trim() };
106
- const idc = prompt(" tt-target-idc (optional, press Enter to skip):");
107
- if (idc && idc.trim().length > 0) cookies["tt-target-idc"] = idc.trim();
108
- writeFileSync(cookiesPath, `${JSON.stringify(cookies, null, 2)}\n`);
109
- console.log();
110
- console.log(`${color.green("✓")} cookies.json created at ./cookies.json`);
111
- console.log(` You can now run: tokrec -u <username>`);
112
- return;
113
- }
114
- const config = result.config;
115
- const firefoxCookies = extractTikTokCookiesFromFirefox();
116
- config.cookies = firefoxCookies ?? await loadCookies(config.cookiesPath);
117
- config.logConsole = false;
118
- const display = createDisplay();
119
- if (config.cookies) {
120
- const source = firefoxCookies ? "Firefox" : "cookies.json";
121
- const names = Object.keys(config.cookies);
122
- const detail = names.length > 1 ? ` (${names.length} cookies)` : "";
123
- display.showInfo(`${source} cookies loaded${detail}`);
124
- } else display.showWarning("No TikTok cookies found — log in at tiktok.com in Firefox or create cookies.json");
125
- const recorder = createRecorder(config);
126
- let lastWasLive = null;
127
- let firstOfflineTime = 0;
128
- recorder.on("checking", (info) => {
129
- display.checkingUser(info.user);
130
- });
131
- recorder.on("tick", (info) => {
132
- if (info.isLive) display.userLive(info.user, info.roomId ?? "?");
133
- else if (lastWasLive === null || lastWasLive === true) {
134
- display.userOffline(info.user);
135
- firstOfflineTime = Date.now();
136
- } else {
137
- const elapsed = Date.now() - firstOfflineTime;
138
- display.userOfflineRepeat(info.user, relativeTime(elapsed));
139
- }
140
- lastWasLive = info.isLive;
141
- });
142
- recorder.on("recording:start", () => {
143
- display.startRecording();
144
- });
145
- recorder.on("download:end", (info) => {
146
- const parsed = info.file.split("/").pop() ?? info.file;
147
- const sizeStr = bytesToHuman(info.size);
148
- display.finishRecording(parsed, info.duration, sizeStr);
149
- });
150
- recorder.on("segmenting:start", () => {
151
- display.startSegmenting();
152
- });
153
- recorder.on("segmenting:end", (info) => {
154
- display.segmentsCreated(info.segments);
155
- });
156
- recorder.on("converting:start", () => {
157
- display.startConverting();
158
- });
159
- recorder.on("converted", (info) => {
160
- const parsed = info.output.split("/").pop() ?? info.output;
161
- display.conversionDone(parsed);
162
- });
163
- recorder.on("recording:end", () => {});
164
- recorder.on("error", (err) => {
165
- display.showError(`[${err.kind}] ${err.message}`);
166
- });
167
- recorder.on("normalize:start", () => {
168
- display.normalizeStart();
169
- });
170
- recorder.on("normalize:progress", (info) => {
171
- display.normalizeProgress(info.percent, info.phase);
172
- });
173
- recorder.on("normalize:end", () => {
174
- display.normalizeComplete();
175
- });
176
- recorder.on("normalize:error", (info) => {
177
- display.normalizeError(info.error);
178
- });
179
- let stopping = false;
180
- const handleSignal = async () => {
181
- if (stopping) return;
182
- stopping = true;
183
- process.off("SIGINT", handleSignal);
184
- process.off("SIGTERM", handleSignal);
185
- process.off("SIGHUP", handleSignal);
186
- display.showInfo("Shutting down gracefully...");
187
- await recorder.stop();
188
- display.stop();
189
- process.exit(0);
190
- };
191
- process.on("SIGINT", handleSignal);
192
- process.on("SIGTERM", handleSignal);
193
- process.on("SIGHUP", handleSignal);
194
- display.pollingStarted(config.interval ?? 3);
195
- try {
196
- await recorder.start();
197
- } catch (err) {
198
- if (err instanceof TikTokError) display.showError(`[${err.kind}] ${err.message}`);
199
- else display.showError(`Unexpected error: ${err instanceof Error ? err.message : String(err)}`);
200
- process.off("SIGINT", handleSignal);
201
- process.off("SIGTERM", handleSignal);
202
- process.off("SIGHUP", handleSignal);
203
- await recorder.stop();
204
- display.stop();
205
- process.exit(1);
206
- }
207
- }
208
- main().catch((err) => {
209
- console.error("Fatal:", err instanceof Error ? err.message : String(err));
210
- process.exit(1);
211
- });
212
- //#endregion
213
- export {};
214
-
215
- //# sourceMappingURL=bin.mjs.map
package/dist/bin.mjs.map DELETED
@@ -1 +0,0 @@
1
- {"version":3,"file":"bin.mjs","names":[],"sources":["../src/bin.ts"],"sourcesContent":["#!/usr/bin/env bun\n/**\n * Entry point — parses CLI args, creates the recorder, and handles signals.\n * Uses the terminal Display manager for beautiful console output.\n */\n\nimport { existsSync, readFileSync, writeFileSync } from \"node:fs\"\nimport { join } from \"node:path\"\nimport { color } from \"@zfadhli/koko-cli\"\nimport { createRecorder } from \"./index\"\nimport { extractTikTokCookiesFromFirefox } from \"./lib/browser-cookies\"\nimport { type CLIResult, parseArgs } from \"./lib/cli\"\nimport type { RecorderConfig } from \"./lib/config\"\nimport { TikTokError } from \"./lib/config\"\nimport { tmuxStart, tmuxStatus, tmuxStop } from \"./lib/tmux\"\nimport { createDisplay } from \"./lib/ui\"\nimport { bytesToHuman, relativeTime } from \"./lib/utils\"\n\nasync function loadCookies(cookiesPath?: string): Promise<Record<string, string> | undefined> {\n const path = cookiesPath ?? join(process.cwd(), \"cookies.json\")\n if (!existsSync(path)) {\n return undefined\n }\n try {\n const content = readFileSync(path, \"utf-8\")\n const data = JSON.parse(content) as Record<string, string | undefined>\n if (data.sessionid_ss && data.sessionid_ss.length > 0) {\n return data as Record<string, string>\n }\n return undefined\n } catch {\n return undefined\n }\n}\n\nasync function main(): Promise<void> {\n let result: CLIResult\n\n try {\n result = parseArgs()\n } catch (err) {\n if (err instanceof TikTokError && err.kind === \"config-error\") {\n console.error(`${color.red(\"Error:\")} ${err.message}`)\n process.exit(1)\n }\n throw err\n }\n\n // Handle tmux subcommands\n if (result.command === \"start\") {\n // Extract user from args to build session name\n const userMatch = result.args.find((_, i, a) => a[i - 1] === \"--user\" || a[i - 1] === \"-u\")\n if (!userMatch) {\n console.error(`${color.red(\"Error:\")} --user is required for \"start\" command`)\n process.exit(1)\n }\n const cmd = `tokrec ${result.args.join(\" \")}`\n try {\n await tmuxStart(userMatch, cmd)\n console.log(`${color.green(\"✓\")} Session \"tokrec-${userMatch}\" started in background`)\n console.log(` tmux attach -t tokrec-${userMatch} — view live output`)\n console.log(` tokrec stop -u ${userMatch} — stop recording`)\n } catch (err) {\n console.error(`${color.red(\"Error:\")} ${err instanceof Error ? err.message : String(err)}`)\n process.exit(1)\n }\n return\n }\n\n if (result.command === \"stop\") {\n try {\n await tmuxStop(result.user)\n if (result.user) {\n console.log(`${color.green(\"✓\")} Session \"tokrec-${result.user}\" stopped`)\n } else {\n console.log(`${color.green(\"✓\")} All tokrec sessions stopped`)\n }\n } catch (err) {\n console.error(`${color.red(\"Error:\")} ${err instanceof Error ? err.message : String(err)}`)\n process.exit(1)\n }\n return\n }\n\n if (result.command === \"status\") {\n const sessions = await tmuxStatus(result.user)\n if (sessions.length === 0) {\n console.log(\n `${color.gray(\"○\")} No active session${result.user ? ` for \"${result.user}\"` : \"\"}`,\n )\n } else {\n for (const s of sessions) {\n console.log(`${color.green(\"●\")} tokrec-${s.user}`)\n console.log(` tmux attach -t ${s.session} — view live output`)\n }\n }\n return\n }\n\n if (result.command === \"setup\") {\n // 1. Check Firefox cookies\n const firefoxCookies = extractTikTokCookiesFromFirefox()\n if (firefoxCookies) {\n const names = Object.keys(firefoxCookies)\n const detail = names.length > 1 ? ` (${names.length} cookies)` : \"\"\n console.log(`${color.green(\"✓\")} Firefox cookies detected${detail}`)\n console.log(` No setup needed — tokrec auto-detects Firefox cookies.`)\n return\n }\n\n // 2. Check cookies.json\n const cookiesPath = join(process.cwd(), \"cookies.json\")\n if (existsSync(cookiesPath)) {\n try {\n const data = JSON.parse(readFileSync(cookiesPath, \"utf-8\"))\n if (data.sessionid_ss) {\n console.log(`${color.green(\"✓\")} cookies.json found at ./cookies.json`)\n return\n }\n } catch {\n // invalid JSON, fall through to setup\n }\n }\n\n // 3. No cookies found — interactive setup\n console.log(`${color.yellow(\"⚠\")} No TikTok cookies found.`)\n console.log()\n console.log(`You need a valid sessionid_ss cookie to bypass TikTok's WAF.`)\n console.log(`Either log in to TikTok in Firefox (auto-detected), or enter manually:`)\n console.log()\n\n const sessionid = prompt(\" sessionid_ss cookie value:\")\n if (!sessionid || sessionid.trim().length === 0) {\n console.log(`${color.red(\"✗\")} No cookie provided. Setup cancelled.`)\n return\n }\n\n const cookies: Record<string, string> = { sessionid_ss: sessionid.trim() }\n\n const idc = prompt(\" tt-target-idc (optional, press Enter to skip):\")\n if (idc && idc.trim().length > 0) {\n cookies[\"tt-target-idc\"] = idc.trim()\n }\n\n writeFileSync(cookiesPath, `${JSON.stringify(cookies, null, 2)}\\n`)\n console.log()\n console.log(`${color.green(\"✓\")} cookies.json created at ./cookies.json`)\n console.log(` You can now run: tokrec -u <username>`)\n return\n }\n\n // Record command — full recorder flow\n const config: RecorderConfig = result.config\n\n // Load cookies: try Firefox browser first, then fall back to cookies.json\n const firefoxCookies = extractTikTokCookiesFromFirefox()\n config.cookies = firefoxCookies ?? (await loadCookies(config.cookiesPath))\n\n // Suppress internal logger's console output — the Display owns the terminal\n config.logConsole = false\n\n // Beautiful terminal display\n const display = createDisplay()\n\n if (config.cookies) {\n const source = firefoxCookies ? \"Firefox\" : \"cookies.json\"\n const names = Object.keys(config.cookies)\n const detail = names.length > 1 ? ` (${names.length} cookies)` : \"\"\n display.showInfo(`${source} cookies loaded${detail}`)\n } else {\n display.showWarning(\n \"No TikTok cookies found — log in at tiktok.com in Firefox or create cookies.json\",\n )\n }\n\n const recorder = createRecorder(config)\n\n let lastWasLive: boolean | null = null\n let firstOfflineTime = 0\n\n // Subscribe to events for beautiful console output\n recorder.on(\"checking\", (info) => {\n display.checkingUser(info.user)\n })\n\n recorder.on(\"tick\", (info) => {\n if (info.isLive) {\n display.userLive(info.user, info.roomId ?? \"?\")\n } else if (lastWasLive === null || lastWasLive === true) {\n display.userOffline(info.user)\n firstOfflineTime = Date.now()\n } else {\n const elapsed = Date.now() - firstOfflineTime\n display.userOfflineRepeat(info.user, relativeTime(elapsed))\n }\n lastWasLive = info.isLive\n })\n\n recorder.on(\"recording:start\", () => {\n display.startRecording()\n })\n\n recorder.on(\"download:end\", (info) => {\n const parsed = info.file.split(\"/\").pop() ?? info.file\n const sizeStr = bytesToHuman(info.size)\n display.finishRecording(parsed, info.duration, sizeStr)\n })\n\n recorder.on(\"segmenting:start\", () => {\n display.startSegmenting()\n })\n\n recorder.on(\"segmenting:end\", (info) => {\n display.segmentsCreated(info.segments)\n })\n\n recorder.on(\"converting:start\", () => {\n display.startConverting()\n })\n\n recorder.on(\"converted\", (info) => {\n const parsed = info.output.split(\"/\").pop() ?? info.output\n display.conversionDone(parsed)\n })\n\n recorder.on(\"recording:end\", () => {\n // For simple conversion (no segmenting), recording:end fires once.\n // For segmenting path, segmenting:end already acknowledges the result.\n // This is a no-op since we handle the terminal output through download:end\n // and segmenting:end / converted events.\n })\n\n recorder.on(\"error\", (err) => {\n display.showError(`[${err.kind}] ${err.message}`)\n })\n\n recorder.on(\"normalize:start\", () => {\n display.normalizeStart()\n })\n\n recorder.on(\"normalize:progress\", (info) => {\n display.normalizeProgress(info.percent, info.phase)\n })\n\n recorder.on(\"normalize:end\", () => {\n display.normalizeComplete()\n })\n\n recorder.on(\"normalize:error\", (info) => {\n display.normalizeError(info.error)\n })\n\n // Signal handling\n let stopping = false\n const handleSignal = async () => {\n if (stopping) return // ignore duplicate signals (e.g. SIGINT + SIGTERM from one Ctrl-C)\n stopping = true\n // Remove signal handlers so they don't accumulate if main() is called again\n process.off(\"SIGINT\", handleSignal)\n process.off(\"SIGTERM\", handleSignal)\n process.off(\"SIGHUP\", handleSignal)\n display.showInfo(\"Shutting down gracefully...\")\n await recorder.stop()\n display.stop()\n process.exit(0)\n }\n\n process.on(\"SIGINT\", handleSignal)\n process.on(\"SIGTERM\", handleSignal)\n process.on(\"SIGHUP\", handleSignal)\n\n // Show initial status and start the recorder\n display.pollingStarted(config.interval ?? 3)\n try {\n await recorder.start()\n } catch (err) {\n if (err instanceof TikTokError) {\n display.showError(`[${err.kind}] ${err.message}`)\n } else {\n display.showError(`Unexpected error: ${err instanceof Error ? err.message : String(err)}`)\n }\n process.off(\"SIGINT\", handleSignal)\n process.off(\"SIGTERM\", handleSignal)\n process.off(\"SIGHUP\", handleSignal)\n await recorder.stop()\n display.stop()\n process.exit(1)\n }\n}\n\nmain().catch((err) => {\n console.error(\"Fatal:\", err instanceof Error ? err.message : String(err))\n process.exit(1)\n})\n"],"mappings":"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"}