@zfadhli/tokrec 0.2.1 → 0.3.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/README.md +315 -0
- package/dist/cli.mjs +25 -4
- package/dist/cli.mjs.map +1 -1
- package/dist/package.mjs +6 -0
- package/dist/package.mjs.map +1 -0
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# tokrec
|
|
4
|
+
|
|
5
|
+
**Minimal TikTok live stream recorder — Bun + TypeScript**
|
|
6
|
+
|
|
7
|
+
<br>
|
|
8
|
+
|
|
9
|
+
[](https://github.com/zfadhli/tokrec/actions/workflows/ci.yml)
|
|
10
|
+
[](https://www.npmjs.com/package/@zfadhli/tokrec)
|
|
11
|
+
[](https://www.npmjs.com/package/@zfadhli/tokrec)
|
|
12
|
+
[](https://bun.sh)
|
|
13
|
+
[](LICENSE)
|
|
14
|
+
|
|
15
|
+
[CLI options](#cli-options) · [Features](#features) · [Installation](#installation) · [Quick start](#quick-start) · [Authentication](#authentication) · [How it works](#how-it-works) · [Library API](#library-api) · [FAQ](#faq)
|
|
16
|
+
|
|
17
|
+
</div>
|
|
18
|
+
|
|
19
|
+
`tokrec` polls a TikTok user's live status, detects when they go live, downloads the FLV stream, and post-processes it with FFmpeg into playable MP4 segments. It is available as both a CLI tool and a TypeScript library.
|
|
20
|
+
|
|
21
|
+
> [!NOTE]
|
|
22
|
+
> 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.
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
- **Automatic polling** — checks every N minutes (default 3) and starts recording as soon as the user goes live.
|
|
29
|
+
- **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.
|
|
30
|
+
- **Time-aligned MP4 segments** — post-recording, FFmpeg splits the raw FLV into configurable-length MP4 segments (default 20 minutes), each independently playable.
|
|
31
|
+
- **Graceful shutdown** — responds to `SIGINT`, `SIGTERM`, and `SIGHUP` (terminal close). Aborts the current download, flushes buffered data, runs FFmpeg, and exits cleanly. A second signal force-kills.
|
|
32
|
+
- **Live progress display** — animated spinner with real-time byte count, elapsed time, and download speed. Built with [@zfadhli/koko-cli](https://github.com/zfadhli/koko-cli).
|
|
33
|
+
- **Cookie-based auth** — seeds a `sessionid_ss` cookie into the HTTP session for authenticated requests, bypassing TikTok's Slardar WAF.
|
|
34
|
+
- **Duration limit** — record for a fixed time (`--duration`) or unlimited (default).
|
|
35
|
+
- **Proxy support** — route traffic through an HTTP proxy for regional access.
|
|
36
|
+
- **Dual entry** — use as a CLI tool or embed `createRecorder()` in your own application.
|
|
37
|
+
- **45+ passing tests** — unit-tested CLI parsing, config validation, stream URL detection, and logger.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
### CLI tool (global)
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm install -g @zfadhli/tokrec
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Then run with:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
tiktok-live-recorder -u username
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
### One-off use (npx)
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
npx @zfadhli/tokrec -u username
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### As a library
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npm install @zfadhli/tokrec
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { createRecorder } from '@zfadhli/tokrec'
|
|
69
|
+
|
|
70
|
+
const recorder = createRecorder({ user: 'username' })
|
|
71
|
+
recorder.on('recording:end', (info) => console.log('Done:', info.file))
|
|
72
|
+
await recorder.start()
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
> [!TIP]
|
|
76
|
+
> 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.
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
## Requirements
|
|
81
|
+
|
|
82
|
+
- **FFmpeg** — must be on your `$PATH` for FLV→MP4 conversion and segmenting.
|
|
83
|
+
- Linux: `apt install ffmpeg`
|
|
84
|
+
- macOS: `brew install ffmpeg`
|
|
85
|
+
- Windows: `choco install ffmpeg`
|
|
86
|
+
- **Bun** (optional, for development) — Node.js 20+ can run the built package.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Quick start
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
# Record a live stream (Ctrl+C to stop)
|
|
94
|
+
tiktok-live-recorder -u vierstinrovve
|
|
95
|
+
|
|
96
|
+
# Record for 10 minutes, split into 5-minute segments
|
|
97
|
+
tiktok-live-recorder -u vierstinrovve -d 10 -s 5
|
|
98
|
+
|
|
99
|
+
# Use a proxy and custom output directory
|
|
100
|
+
tiktok-live-recorder -u vierstinrovve -o ./videos -p http://127.0.0.1:8080
|
|
101
|
+
|
|
102
|
+
# Specify a cookies file (see Authentication section)
|
|
103
|
+
tiktok-live-recorder -u vierstinrovve -c ./my-cookies.json
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## CLI options
|
|
109
|
+
|
|
110
|
+
| Option | Shorthand | Default | Description |
|
|
111
|
+
|--------|-----------|---------|-------------|
|
|
112
|
+
| `--user <name>` | `-u` | _(required)_ | TikTok username (without `@`) |
|
|
113
|
+
| `--output <dir>` | `-o` | `./recordings` | Output directory for recordings |
|
|
114
|
+
| `--interval <minutes>` | `-i` | `3` | Polling interval (minimum 1) |
|
|
115
|
+
| `--duration <minutes>` | `-d` | unlimited | Maximum recording duration |
|
|
116
|
+
| `--segment-minutes <minutes>` | `-s` | `20` | Length of each output MP4 segment |
|
|
117
|
+
| `--proxy <url>` | `-p` | none | HTTP proxy URL |
|
|
118
|
+
| `--cookies <path>` | `-c` | `./cookies.json` | Path to cookies JSON file |
|
|
119
|
+
| `--log-level <level>` | `-l` | `info` | One of: `debug`, `info`, `warn`, `error` |
|
|
120
|
+
|
|
121
|
+
> [!TIP]
|
|
122
|
+
> `--duration` and `--segment-minutes` both accept values in **minutes**. The tool converts them internally.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Authentication
|
|
127
|
+
|
|
128
|
+
TikTok's Slardar WAF blocks unauthenticated requests. You must provide a valid `sessionid_ss` cookie to bypass it.
|
|
129
|
+
|
|
130
|
+
### Setup
|
|
131
|
+
|
|
132
|
+
1. Open TikTok in your browser and log in.
|
|
133
|
+
2. Open DevTools → **Application** → **Cookies** → `www.tiktok.com`.
|
|
134
|
+
3. Copy the value of `sessionid_ss` and create a `cookies.json` file:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{
|
|
138
|
+
"sessionid_ss": "6c74daa215a8f34eeddf5162ca091668"
|
|
139
|
+
}
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
You can also include the `tt-target-idc` cookie (optional, for regional routing):
|
|
143
|
+
|
|
144
|
+
```json
|
|
145
|
+
{
|
|
146
|
+
"sessionid_ss": "6c74daa215a8f34eeddf5162ca091668",
|
|
147
|
+
"tt-target-idc": "useast2a"
|
|
148
|
+
}
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
4. Run the tool with `--cookies ./path/to/cookies.json`, or place `cookies.json` in the working directory (auto-detected).
|
|
152
|
+
|
|
153
|
+
> [!CAUTION]
|
|
154
|
+
> Without valid cookies, the tool will report every user as **offline** because the WAF challenge page won't contain live-stream JSON data.
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## How it works
|
|
159
|
+
|
|
160
|
+
### Pipeline
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
┌────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
164
|
+
│ Poll tick │ → │ Detect live │ → │ Download FLV │ → │ FFmpeg post │
|
|
165
|
+
│ (every Nm) │ │ via SIGI_ │ │ (auto-reconn.)│ │ (segment/ │
|
|
166
|
+
│ │ │ STATE scrape │ │ │ │ convert) │
|
|
167
|
+
└────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
1. **Poll tick** — every N minutes, the monitor calls the check function.
|
|
171
|
+
2. **Live detection** — the TikTok profile page is fetched and the `SIGI_STATE` JSON blob is extracted. Stream URL extraction follows a three-tier fallback:
|
|
172
|
+
- Fast-path from the known `pull_data.stream_data` location (~95% of cases).
|
|
173
|
+
- Brute-force recursive search of the entire JSON (survives key name changes).
|
|
174
|
+
- Webcast API fallback (`/webcast/room/info/?aid=1988`).
|
|
175
|
+
3. **Download** — the raw FLV is streamed to disk with a 512 KB buffer and 60-second read timeout. If the stream segment ends, the downloader fetches a fresh URL and continues (up to 100 reconnects per session). The `--duration` limit is checked against total elapsed time across reconnections.
|
|
176
|
+
4. **FFmpeg post-processing** — the FLV is split into time-aligned MP4 segments using `ffmpeg -c copy -f segment -segment_time N -reset_timestamps 1`. If segmenting fails, it falls back to a simple FLV→MP4 conversion.
|
|
177
|
+
|
|
178
|
+
### Event system
|
|
179
|
+
|
|
180
|
+
The recorder emits typed events that you can subscribe to via `recorder.on()`:
|
|
181
|
+
|
|
182
|
+
| Event | Payload | When |
|
|
183
|
+
|-------|---------|------|
|
|
184
|
+
| `checking` | `{ user }` | Before each poll tick |
|
|
185
|
+
| `tick` | `{ user, isLive, roomId? }` | After checking live status |
|
|
186
|
+
| `recording:start` | `{ user, file }` | When recording begins |
|
|
187
|
+
| `download:progress` | `{ bytes, elapsed, speed }` | During download (~1s intervals) |
|
|
188
|
+
| `download:end` | `{ file, duration, size }` | When download completes |
|
|
189
|
+
| `segmenting:start` | `{ input, outputPattern }` | Before FFmpeg segmenting |
|
|
190
|
+
| `segmenting:end` | `{ segments }` | After segmenting |
|
|
191
|
+
| `converting:start` | `{ input }` | Before simple conversion |
|
|
192
|
+
| `converted` | `{ input, output }` | After each segment/file |
|
|
193
|
+
| `error` | `err: TikTokError` | On non-fatal errors |
|
|
194
|
+
|
|
195
|
+
### Output structure
|
|
196
|
+
|
|
197
|
+
Recordings are saved as:
|
|
198
|
+
|
|
199
|
+
```
|
|
200
|
+
./recordings/
|
|
201
|
+
vierstinrovve=20260609_143000.flv # raw download (deleted after conversion)
|
|
202
|
+
vierstinrovve=20260609_143000_part1.mp4 # segment 1
|
|
203
|
+
vierstinrovve=20260609_143000_part2.mp4 # segment 2
|
|
204
|
+
...
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
File naming: `{username}={YYYYMMDD}_{HHMMSS}[_partN].mp4`
|
|
208
|
+
|
|
209
|
+
---
|
|
210
|
+
|
|
211
|
+
## Library API
|
|
212
|
+
|
|
213
|
+
### `createRecorder(config)`
|
|
214
|
+
|
|
215
|
+
```ts
|
|
216
|
+
import { createRecorder } from '@zfadhli/tokrec'
|
|
217
|
+
import type { RecorderConfig, RecorderController, RecorderEvent } from '@zfadhli/tokrec'
|
|
218
|
+
|
|
219
|
+
const recorder: RecorderController = createRecorder({
|
|
220
|
+
user: 'username', // required
|
|
221
|
+
outputDir: './videos', // default: './recordings'
|
|
222
|
+
interval: 3, // polling interval in minutes
|
|
223
|
+
duration: 600, // max recording duration in seconds
|
|
224
|
+
segmentMinutes: 20, // segment length in minutes
|
|
225
|
+
proxy: 'http://127.0.0.1:8080',
|
|
226
|
+
cookies: { sessionid_ss: '...' },
|
|
227
|
+
logLevel: 'info',
|
|
228
|
+
logConsole: true,
|
|
229
|
+
})
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
### `RecorderController`
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
interface RecorderController {
|
|
236
|
+
start(): Promise<void>
|
|
237
|
+
stop(): Promise<void>
|
|
238
|
+
getStatus(): RecorderStatus
|
|
239
|
+
on<E extends RecorderEvent>(event: E, handler: RecorderEventHandler[E]): void
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### `RecorderStatus`
|
|
244
|
+
|
|
245
|
+
```ts
|
|
246
|
+
interface RecorderStatus {
|
|
247
|
+
state: 'idle' | 'polling' | 'recording' | 'converting' | 'stopped'
|
|
248
|
+
user: string
|
|
249
|
+
currentFile?: string
|
|
250
|
+
sessionDuration?: number
|
|
251
|
+
lastPollTime?: string
|
|
252
|
+
lastError?: string
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
### Example: watch for live status
|
|
257
|
+
|
|
258
|
+
```ts
|
|
259
|
+
import { createRecorder } from '@zfadhli/tokrec'
|
|
260
|
+
|
|
261
|
+
const recorder = createRecorder({ user: 'username' })
|
|
262
|
+
|
|
263
|
+
recorder.on('checking', ({ user }) => console.log(`Checking @${user}...`))
|
|
264
|
+
recorder.on('tick', ({ isLive, roomId, user }) => {
|
|
265
|
+
console.log(isLive ? `@${user} is LIVE!` : `@${user} is offline`)
|
|
266
|
+
})
|
|
267
|
+
|
|
268
|
+
recorder.on('error', (err) => console.error(`[${err.kind}] ${err.message}`))
|
|
269
|
+
|
|
270
|
+
await recorder.start()
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
---
|
|
274
|
+
|
|
275
|
+
## FAQ
|
|
276
|
+
|
|
277
|
+
### Why does the tool say "offline" for every user?
|
|
278
|
+
|
|
279
|
+
You likely don't have a valid `sessionid_ss` cookie. See the [Authentication](#authentication) section.
|
|
280
|
+
|
|
281
|
+
### Why does the download stop after 30 seconds?
|
|
282
|
+
|
|
283
|
+
TikTok serves live streams as short FLV segments (~30–60s). The tool now reconnects automatically, but older versions stopped after one segment. Update to the latest version.
|
|
284
|
+
|
|
285
|
+
### Can I record multiple users at the same time?
|
|
286
|
+
|
|
287
|
+
Run multiple instances of the CLI in separate terminals, each with a different `--user` and `--output`.
|
|
288
|
+
|
|
289
|
+
### Can I use this with Node.js instead of Bun?
|
|
290
|
+
|
|
291
|
+
The package is built to ESM for Node.js 20+. The runtime dependency is `@zfadhli/koko-cli` and `wreq-js`, both of which work in Node. Bun is optional for development.
|
|
292
|
+
|
|
293
|
+
### Where are the log files?
|
|
294
|
+
|
|
295
|
+
`tiktok-recorder.log` in the working directory, with rotating backups. File logs include full timestamps and levels for debugging.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Troubleshooting
|
|
300
|
+
|
|
301
|
+
### FFmpeg not found
|
|
302
|
+
|
|
303
|
+
Install FFmpeg and ensure it is on your `$PATH`:
|
|
304
|
+
|
|
305
|
+
```bash
|
|
306
|
+
which ffmpeg # should print a path
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
### "WAF blocked" errors
|
|
310
|
+
|
|
311
|
+
Your `sessionid_ss` cookie has expired or is invalid. Generate a fresh one from your browser's DevTools.
|
|
312
|
+
|
|
313
|
+
### The spinner freezes during download
|
|
314
|
+
|
|
315
|
+
The download has a 60-second read timeout. If TikTok stops sending data (e.g., network drop), the tool waits 60 seconds before attempting reconnection. If reconnection fails, the download ends with whatever data was buffered.
|
package/dist/cli.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { TikTokError } from "./config.mjs";
|
|
2
2
|
import { sanitizeUser } from "./utils.mjs";
|
|
3
|
+
import { version } from "./package.mjs";
|
|
3
4
|
import { createCLI } from "@zfadhli/koko-cli";
|
|
4
5
|
//#region src/cli.ts
|
|
5
6
|
/**
|
|
@@ -9,9 +10,9 @@ import { createCLI } from "@zfadhli/koko-cli";
|
|
|
9
10
|
function parseArgs(argv = process.argv) {
|
|
10
11
|
let config;
|
|
11
12
|
let error;
|
|
12
|
-
const cli = createCLI("tiktok-live-recorder",
|
|
13
|
+
const cli = createCLI("tiktok-live-recorder", version).description("Minimal TikTok live stream recorder — Bun + TypeScript");
|
|
13
14
|
cli.command("", "Record a TikTok live stream", (cmd) => {
|
|
14
|
-
cmd.option("-u, --user <name>", "TikTok username (
|
|
15
|
+
cmd.option("-u, --user <name>", "TikTok username (or pass as first argument)");
|
|
15
16
|
cmd.option("-o, --output <dir>", "Output directory", { default: "./recordings" });
|
|
16
17
|
cmd.option("-i, --interval <minutes>", "Polling interval in minutes", { default: "3" });
|
|
17
18
|
cmd.option("-d, --duration <minutes>", "Max recording duration in minutes");
|
|
@@ -22,7 +23,7 @@ function parseArgs(argv = process.argv) {
|
|
|
22
23
|
cmd.action((opts) => {
|
|
23
24
|
try {
|
|
24
25
|
const user = opts.user;
|
|
25
|
-
if (!user) throw new TikTokError("config-error", "
|
|
26
|
+
if (!user) throw new TikTokError("config-error", "<user> is required (positional or --user)");
|
|
26
27
|
const parsed = { user: sanitizeUser(user) };
|
|
27
28
|
if (opts.output) parsed.outputDir = opts.output;
|
|
28
29
|
if (opts.interval !== void 0) {
|
|
@@ -58,11 +59,31 @@ function parseArgs(argv = process.argv) {
|
|
|
58
59
|
}
|
|
59
60
|
});
|
|
60
61
|
});
|
|
62
|
+
const positionalUser = extractPositional(argv);
|
|
63
|
+
if (positionalUser) argv.push("--user", positionalUser);
|
|
61
64
|
cli.parse(argv);
|
|
62
65
|
if (error) throw new TikTokError("config-error", error);
|
|
63
|
-
if (!config) throw new TikTokError("config-error", "
|
|
66
|
+
if (!config) throw new TikTokError("config-error", "<user> is required (positional or --user). Use --help for usage.");
|
|
64
67
|
return config;
|
|
65
68
|
}
|
|
69
|
+
/**
|
|
70
|
+
* Scan argv for the first bare argument that is neither a flag nor a flag's value.
|
|
71
|
+
* Returns the value and removes it from the array, or null if none found.
|
|
72
|
+
*/
|
|
73
|
+
function extractPositional(argv) {
|
|
74
|
+
for (let i = 2; i < argv.length; i++) {
|
|
75
|
+
const arg = argv[i];
|
|
76
|
+
if (arg === "--") return argv.splice(i, 1)[0];
|
|
77
|
+
if (arg.startsWith("-")) {
|
|
78
|
+
const next = argv[i + 1];
|
|
79
|
+
if (next && !next.startsWith("-") && arg !== "--help" && arg !== "--version") i++;
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
argv.splice(i, 1);
|
|
83
|
+
return arg;
|
|
84
|
+
}
|
|
85
|
+
return null;
|
|
86
|
+
}
|
|
66
87
|
//#endregion
|
|
67
88
|
export { parseArgs };
|
|
68
89
|
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.mjs","names":[],"sources":["../src/cli.ts"],"sourcesContent":["/**\n * CLI argument parser — uses koko-cli's createCLI (wraps cac).\n * Exports a single parseArgs() function.\n */\n\nimport { createCLI } from '@zfadhli/koko-cli'\nimport type { LogLevel, RecorderConfig } from './config'\nimport { TikTokError } from './config'\nimport { sanitizeUser } from './utils'\n\nexport function parseArgs(argv: string[] = process.argv): RecorderConfig {\n let config: RecorderConfig | undefined\n let error: string | undefined\n\n const cli = createCLI('tiktok-live-recorder',
|
|
1
|
+
{"version":3,"file":"cli.mjs","names":["pkg.version"],"sources":["../src/cli.ts"],"sourcesContent":["/**\n * CLI argument parser — uses koko-cli's createCLI (wraps cac).\n * Exports a single parseArgs() function.\n */\n\nimport { createCLI } from '@zfadhli/koko-cli'\nimport pkg from '../package.json'\nimport type { LogLevel, RecorderConfig } from './config'\nimport { TikTokError } from './config'\nimport { sanitizeUser } from './utils'\n\nexport function parseArgs(argv: string[] = process.argv): RecorderConfig {\n let config: RecorderConfig | undefined\n let error: string | undefined\n\n const cli = createCLI('tiktok-live-recorder', pkg.version).description(\n 'Minimal TikTok live stream recorder — Bun + TypeScript',\n )\n\n cli.command('', 'Record a TikTok live stream', (cmd) => {\n cmd.option('-u, --user <name>', 'TikTok username (or pass as first argument)')\n cmd.option('-o, --output <dir>', 'Output directory', { default: './recordings' })\n cmd.option('-i, --interval <minutes>', 'Polling interval in minutes', { default: '3' })\n cmd.option('-d, --duration <minutes>', 'Max recording duration in minutes')\n cmd.option('-p, --proxy <url>', 'HTTP proxy (e.g. http://127.0.0.1:8080)')\n cmd.option('-l, --log-level <level>', 'Log level: debug | info | warn | error')\n cmd.option('-c, --cookies <path>', 'Path to cookies.json')\n cmd.option('-s, --segment-minutes <minutes>', 'Segment duration in minutes', { default: '20' })\n\n cmd.action((opts: Record<string, unknown>) => {\n try {\n const user = opts.user as string | undefined\n if (!user) {\n throw new TikTokError('config-error', '<user> is required (positional or --user)')\n }\n\n const parsed: RecorderConfig = {\n user: sanitizeUser(user),\n }\n\n if (opts.output) parsed.outputDir = opts.output as string\n if (opts.interval !== undefined) {\n const n = Number(opts.interval)\n if (!Number.isFinite(n) || n < 1) {\n throw new TikTokError('config-error', '--interval must be a number >= 1')\n }\n parsed.interval = n\n }\n if (opts.duration !== undefined) {\n const n = Number(opts.duration)\n if (!Number.isFinite(n) || n < 0) {\n throw new TikTokError('config-error', '--duration must be a number >= 0 (minutes)')\n }\n parsed.duration = n * 60\n }\n if (opts.proxy) parsed.proxy = opts.proxy as string\n if (opts.logLevel) {\n const levels = ['debug', 'info', 'warn', 'error']\n if (!levels.includes(opts.logLevel as string)) {\n throw new TikTokError(\n 'config-error',\n `--log-level must be one of: ${levels.join(', ')}`,\n )\n }\n parsed.logLevel = opts.logLevel as LogLevel\n }\n if (opts.cookies) parsed.cookiesPath = opts.cookies as string\n if (opts.segmentMinutes !== undefined) {\n const n = Number(opts.segmentMinutes)\n if (!Number.isFinite(n) || n < 1) {\n throw new TikTokError('config-error', '--segment-minutes must be a number >= 1')\n }\n parsed.segmentMinutes = n\n }\n\n config = parsed\n } catch (err) {\n error = err instanceof Error ? err.message : String(err)\n }\n })\n })\n\n // If the first bare argument (not a flag or flag value) looks like a username,\n // convert it to --user for cac (which only handles options on the default command).\n const positionalUser = extractPositional(argv)\n if (positionalUser) {\n argv.push('--user', positionalUser)\n }\n\n cli.parse(argv)\n\n if (error) {\n throw new TikTokError('config-error', error)\n }\n\n if (!config) {\n throw new TikTokError(\n 'config-error',\n '<user> is required (positional or --user). Use --help for usage.',\n )\n }\n\n return config\n}\n\n/**\n * Scan argv for the first bare argument that is neither a flag nor a flag's value.\n * Returns the value and removes it from the array, or null if none found.\n */\nfunction extractPositional(argv: string[]): string | null {\n for (let i = 2; i < argv.length; i++) {\n const arg = argv[i]!\n // -- signals end of options; anything after is a positional\n if (arg === '--') {\n const pos = argv.splice(i, 1)[0]!\n return pos\n }\n // If this arg is a flag (-*) the next arg is its value\n if (arg.startsWith('-')) {\n // Skip the value if this flag takes one (not a boolean flag like --help/--version)\n const next = argv[i + 1]\n if (next && !next.startsWith('-') && arg !== '--help' && arg !== '--version') {\n i++ // skip the value\n }\n continue\n }\n // Found a bare positional — remove it and return\n argv.splice(i, 1)\n return arg\n }\n return null\n}\n"],"mappings":";;;;;;;;;AAWA,SAAgB,UAAU,OAAiB,QAAQ,MAAsB;CACvE,IAAI;CACJ,IAAI;CAEJ,MAAM,MAAM,UAAU,wBAAwBA,OAAW,CAAC,CAAC,YACzD,wDACF;CAEA,IAAI,QAAQ,IAAI,gCAAgC,QAAQ;EACtD,IAAI,OAAO,qBAAqB,6CAA6C;EAC7E,IAAI,OAAO,sBAAsB,oBAAoB,EAAE,SAAS,eAAe,CAAC;EAChF,IAAI,OAAO,4BAA4B,+BAA+B,EAAE,SAAS,IAAI,CAAC;EACtF,IAAI,OAAO,4BAA4B,mCAAmC;EAC1E,IAAI,OAAO,qBAAqB,yCAAyC;EACzE,IAAI,OAAO,2BAA2B,wCAAwC;EAC9E,IAAI,OAAO,wBAAwB,sBAAsB;EACzD,IAAI,OAAO,mCAAmC,+BAA+B,EAAE,SAAS,KAAK,CAAC;EAE9F,IAAI,QAAQ,SAAkC;GAC5C,IAAI;IACF,MAAM,OAAO,KAAK;IAClB,IAAI,CAAC,MACH,MAAM,IAAI,YAAY,gBAAgB,2CAA2C;IAGnF,MAAM,SAAyB,EAC7B,MAAM,aAAa,IAAI,EACzB;IAEA,IAAI,KAAK,QAAQ,OAAO,YAAY,KAAK;IACzC,IAAI,KAAK,aAAa,KAAA,GAAW;KAC/B,MAAM,IAAI,OAAO,KAAK,QAAQ;KAC9B,IAAI,CAAC,OAAO,SAAS,CAAC,KAAK,IAAI,GAC7B,MAAM,IAAI,YAAY,gBAAgB,kCAAkC;KAE1E,OAAO,WAAW;IACpB;IACA,IAAI,KAAK,aAAa,KAAA,GAAW;KAC/B,MAAM,IAAI,OAAO,KAAK,QAAQ;KAC9B,IAAI,CAAC,OAAO,SAAS,CAAC,KAAK,IAAI,GAC7B,MAAM,IAAI,YAAY,gBAAgB,4CAA4C;KAEpF,OAAO,WAAW,IAAI;IACxB;IACA,IAAI,KAAK,OAAO,OAAO,QAAQ,KAAK;IACpC,IAAI,KAAK,UAAU;KACjB,MAAM,SAAS;MAAC;MAAS;MAAQ;MAAQ;KAAO;KAChD,IAAI,CAAC,OAAO,SAAS,KAAK,QAAkB,GAC1C,MAAM,IAAI,YACR,gBACA,+BAA+B,OAAO,KAAK,IAAI,GACjD;KAEF,OAAO,WAAW,KAAK;IACzB;IACA,IAAI,KAAK,SAAS,OAAO,cAAc,KAAK;IAC5C,IAAI,KAAK,mBAAmB,KAAA,GAAW;KACrC,MAAM,IAAI,OAAO,KAAK,cAAc;KACpC,IAAI,CAAC,OAAO,SAAS,CAAC,KAAK,IAAI,GAC7B,MAAM,IAAI,YAAY,gBAAgB,yCAAyC;KAEjF,OAAO,iBAAiB;IAC1B;IAEA,SAAS;GACX,SAAS,KAAK;IACZ,QAAQ,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;GACzD;EACF,CAAC;CACH,CAAC;CAID,MAAM,iBAAiB,kBAAkB,IAAI;CAC7C,IAAI,gBACF,KAAK,KAAK,UAAU,cAAc;CAGpC,IAAI,MAAM,IAAI;CAEd,IAAI,OACF,MAAM,IAAI,YAAY,gBAAgB,KAAK;CAG7C,IAAI,CAAC,QACH,MAAM,IAAI,YACR,gBACA,kEACF;CAGF,OAAO;AACT;;;;;AAMA,SAAS,kBAAkB,MAA+B;CACxD,KAAK,IAAI,IAAI,GAAG,IAAI,KAAK,QAAQ,KAAK;EACpC,MAAM,MAAM,KAAK;EAEjB,IAAI,QAAQ,MAEV,OADY,KAAK,OAAO,GAAG,CAAC,CAAC,CAAC;EAIhC,IAAI,IAAI,WAAW,GAAG,GAAG;GAEvB,MAAM,OAAO,KAAK,IAAI;GACtB,IAAI,QAAQ,CAAC,KAAK,WAAW,GAAG,KAAK,QAAQ,YAAY,QAAQ,aAC/D;GAEF;EACF;EAEA,KAAK,OAAO,GAAG,CAAC;EAChB,OAAO;CACT;CACA,OAAO;AACT"}
|
package/dist/package.mjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"package.mjs","names":[],"sources":["../package.json"],"sourcesContent":[""],"mappings":""}
|