@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.
- 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/README.md +0 -430
- 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
DELETED
|
@@ -1,430 +0,0 @@
|
|
|
1
|
-
<div align="center">
|
|
2
|
-
|
|
3
|
-
# tokrec
|
|
4
|
-
|
|
5
|
-
**Minimal TikTok live stream recorder — Bun + TypeScript**
|
|
6
|
-
|
|
7
|
-
[](https://github.com/zfadhli/tokrec/actions/workflows/ci.yml)
|
|
8
|
-
[](https://www.npmjs.com/package/@zfadhli/tokrec)
|
|
9
|
-
[](https://www.npmjs.com/package/@zfadhli/tokrec)
|
|
10
|
-
[](https://bun.sh)
|
|
11
|
-
[](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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"}
|