@zfadhli/tokrec 0.2.1
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/api/client.mjs +40 -0
- package/dist/api/client.mjs.map +1 -0
- package/dist/api/tiktok.mjs +144 -0
- package/dist/api/tiktok.mjs.map +1 -0
- package/dist/cli.mjs +69 -0
- package/dist/cli.mjs.map +1 -0
- package/dist/config.d.mts +104 -0
- package/dist/config.d.mts.map +1 -0
- package/dist/config.mjs +42 -0
- package/dist/config.mjs.map +1 -0
- package/dist/index.d.mts +1 -0
- package/dist/index.mjs +107 -0
- package/dist/index.mjs.map +1 -0
- package/dist/lib.d.mts +3 -0
- package/dist/lib.mjs +2 -0
- package/dist/logger.mjs +84 -0
- package/dist/logger.mjs.map +1 -0
- package/dist/monitor.mjs +43 -0
- package/dist/monitor.mjs.map +1 -0
- package/dist/recorder/convert.mjs +67 -0
- package/dist/recorder/convert.mjs.map +1 -0
- package/dist/recorder/index.d.mts +7 -0
- package/dist/recorder/index.d.mts.map +1 -0
- package/dist/recorder/index.mjs +267 -0
- package/dist/recorder/index.mjs.map +1 -0
- package/dist/recorder/stream.mjs +171 -0
- package/dist/recorder/stream.mjs.map +1 -0
- package/dist/ui.mjs +114 -0
- package/dist/ui.mjs.map +1 -0
- package/dist/utils.mjs +39 -0
- package/dist/utils.mjs.map +1 -0
- package/package.json +58 -0
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { bytesToHuman, ensureDir, formatFilename } from "../utils.mjs";
|
|
2
|
+
import { createWriteStream } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
//#region src/recorder/stream.ts
|
|
5
|
+
/**
|
|
6
|
+
* Stream downloader — fetches a FLV stream and writes it to disk with buffering.
|
|
7
|
+
* Supports transparent reconnection when TikTok stream segments end naturally.
|
|
8
|
+
*/
|
|
9
|
+
function createStreamDownloader(logger) {
|
|
10
|
+
let abortFlag = false;
|
|
11
|
+
let streamReader = null;
|
|
12
|
+
function abort() {
|
|
13
|
+
abortFlag = true;
|
|
14
|
+
streamReader?.cancel().catch(() => {});
|
|
15
|
+
}
|
|
16
|
+
async function openReader(url) {
|
|
17
|
+
const response = await fetch(url);
|
|
18
|
+
if (!response.ok || !response.body) throw new Error(`Failed to fetch stream: ${response.status}`);
|
|
19
|
+
return response.body.getReader();
|
|
20
|
+
}
|
|
21
|
+
async function download(liveUrl, user, outputDir, maxDuration = 0, onProgress, getNextUrl) {
|
|
22
|
+
abortFlag = false;
|
|
23
|
+
ensureDir(outputDir);
|
|
24
|
+
const filename = formatFilename(user, "flv");
|
|
25
|
+
const filepath = join(outputDir, filename);
|
|
26
|
+
logger?.info(`Recording: ${filename}`);
|
|
27
|
+
const startTime = Date.now();
|
|
28
|
+
let totalBytes = 0;
|
|
29
|
+
let lastProgressTime = 0;
|
|
30
|
+
let reader;
|
|
31
|
+
try {
|
|
32
|
+
reader = await openReader(liveUrl);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
throw new Error(`Failed to open stream: ${err instanceof Error ? err.message : String(err)}`);
|
|
35
|
+
}
|
|
36
|
+
streamReader = reader;
|
|
37
|
+
const writer = createWriteStream(filepath);
|
|
38
|
+
/** Shared across try (read loop) and catch (error handler). */
|
|
39
|
+
function elapsed() {
|
|
40
|
+
return (Date.now() - startTime) / 1e3;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const bufferSize = 512 * 1024;
|
|
44
|
+
let buffer = Buffer.alloc(0);
|
|
45
|
+
function reportProgress(bytes) {
|
|
46
|
+
if (!onProgress) return;
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const e = elapsed();
|
|
49
|
+
const speed = e > 0 ? bytes / e : 0;
|
|
50
|
+
if (now - lastProgressTime >= 1e3) {
|
|
51
|
+
lastProgressTime = now;
|
|
52
|
+
onProgress({
|
|
53
|
+
bytes,
|
|
54
|
+
elapsed: e,
|
|
55
|
+
speed
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/** Try to reconnect to a fresh stream URL. Returns true if reconnected. */
|
|
60
|
+
async function tryReconnect() {
|
|
61
|
+
if (abortFlag) return false;
|
|
62
|
+
if (!getNextUrl) return false;
|
|
63
|
+
if (maxDuration > 0 && elapsed() >= maxDuration) return false;
|
|
64
|
+
logger?.info("Stream ended, attempting reconnection...");
|
|
65
|
+
const nextUrl = await getNextUrl();
|
|
66
|
+
if (!nextUrl) {
|
|
67
|
+
logger?.info("No new stream URL — recording finished");
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
logger?.info(`Reconnecting to new stream URL: ${nextUrl}`);
|
|
71
|
+
try {
|
|
72
|
+
reader.cancel().catch(() => {});
|
|
73
|
+
reader = await openReader(nextUrl);
|
|
74
|
+
streamReader = reader;
|
|
75
|
+
logger?.info("Reconnected successfully");
|
|
76
|
+
return true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
logger?.error(`Reconnection failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
while (!abortFlag) {
|
|
83
|
+
let chunk;
|
|
84
|
+
try {
|
|
85
|
+
chunk = await timeout(reader.read(), 6e4, "Stream read timed out");
|
|
86
|
+
} catch (err) {
|
|
87
|
+
logger?.info("Read timed out, attempting reconnection...");
|
|
88
|
+
if (await tryReconnect()) continue;
|
|
89
|
+
throw err;
|
|
90
|
+
}
|
|
91
|
+
if (chunk.done) {
|
|
92
|
+
if (await tryReconnect()) continue;
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
buffer = Buffer.concat([buffer, Buffer.from(chunk.value)]);
|
|
96
|
+
if (buffer.length >= bufferSize) {
|
|
97
|
+
await writeBuffer(writer, buffer);
|
|
98
|
+
totalBytes += buffer.length;
|
|
99
|
+
buffer = Buffer.alloc(0);
|
|
100
|
+
}
|
|
101
|
+
if (maxDuration > 0) {
|
|
102
|
+
if (elapsed() >= maxDuration) {
|
|
103
|
+
logger?.info(`Duration limit reached (${maxDuration}s)`);
|
|
104
|
+
break;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
reportProgress(totalBytes);
|
|
108
|
+
}
|
|
109
|
+
if (buffer.length > 0) {
|
|
110
|
+
await writeBuffer(writer, buffer);
|
|
111
|
+
totalBytes += buffer.length;
|
|
112
|
+
}
|
|
113
|
+
await new Promise((resolve) => writer.end(resolve));
|
|
114
|
+
const duration = elapsed();
|
|
115
|
+
if (onProgress) onProgress({
|
|
116
|
+
bytes: totalBytes,
|
|
117
|
+
elapsed: duration,
|
|
118
|
+
speed: duration > 0 ? totalBytes / duration : 0
|
|
119
|
+
});
|
|
120
|
+
logger?.info(`Recording finished: ${filename} (${formatDuration(duration)}, ${bytesToHuman(totalBytes)})`);
|
|
121
|
+
return {
|
|
122
|
+
file: filepath,
|
|
123
|
+
duration,
|
|
124
|
+
size: totalBytes
|
|
125
|
+
};
|
|
126
|
+
} catch (err) {
|
|
127
|
+
abort();
|
|
128
|
+
const duration = elapsed();
|
|
129
|
+
logger?.error(`Recording error: ${err instanceof Error ? err.message : String(err)}`);
|
|
130
|
+
return {
|
|
131
|
+
file: filepath,
|
|
132
|
+
duration,
|
|
133
|
+
size: totalBytes
|
|
134
|
+
};
|
|
135
|
+
} finally {
|
|
136
|
+
streamReader = null;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
download,
|
|
141
|
+
abort
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
function writeBuffer(writer, buffer) {
|
|
145
|
+
return new Promise((resolve, reject) => {
|
|
146
|
+
writer.write(buffer, (err) => {
|
|
147
|
+
if (err) reject(err);
|
|
148
|
+
else resolve();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
function formatDuration(seconds) {
|
|
153
|
+
return `${Math.floor(seconds / 60)}m ${Math.floor(seconds % 60)}s`;
|
|
154
|
+
}
|
|
155
|
+
/** Race a promise against a timeout. The losing promise is ignored (no unhandled rejection). */
|
|
156
|
+
function timeout(promise, ms, label) {
|
|
157
|
+
return new Promise((resolve, reject) => {
|
|
158
|
+
const timer = setTimeout(() => reject(new Error(label)), ms);
|
|
159
|
+
promise.then((result) => {
|
|
160
|
+
clearTimeout(timer);
|
|
161
|
+
resolve(result);
|
|
162
|
+
}, (err) => {
|
|
163
|
+
clearTimeout(timer);
|
|
164
|
+
reject(err);
|
|
165
|
+
});
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
//#endregion
|
|
169
|
+
export { createStreamDownloader };
|
|
170
|
+
|
|
171
|
+
//# sourceMappingURL=stream.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"stream.mjs","names":[],"sources":["../../src/recorder/stream.ts"],"sourcesContent":["/**\n * Stream downloader — fetches a FLV stream and writes it to disk with buffering.\n * Supports transparent reconnection when TikTok stream segments end naturally.\n */\n\nimport { type WriteStream, createWriteStream } from 'node:fs'\nimport { join } from 'node:path'\nimport type { Logger } from '../logger'\nimport { bytesToHuman, ensureDir, formatFilename } from '../utils'\n\nexport interface ProgressInfo {\n bytes: number\n elapsed: number\n speed: number\n}\n\nexport interface DownloadResult {\n file: string\n duration: number\n size: number\n}\n\nexport interface StreamDownloader {\n /** Download a live stream. Resolves when the stream ends or is aborted. */\n download: (\n liveUrl: string,\n user: string,\n outputDir: string,\n maxDuration?: number,\n onProgress?: (info: ProgressInfo) => void,\n /** Called when a stream segment ends and we attempt to fetch a fresh URL. */\n getNextUrl?: () => Promise<string | null>,\n ) => Promise<DownloadResult>\n /** Abort an active download */\n abort: () => void\n}\n\nexport function createStreamDownloader(logger?: Logger): StreamDownloader {\n let abortFlag = false\n let streamReader: ReadableStreamDefaultReader<Uint8Array> | null = null\n\n function abort(): void {\n abortFlag = true\n streamReader?.cancel().catch(() => {})\n }\n\n async function openReader(url: string): Promise<ReadableStreamDefaultReader<Uint8Array>> {\n const response = await fetch(url)\n if (!response.ok || !response.body) {\n throw new Error(`Failed to fetch stream: ${response.status}`)\n }\n return response.body.getReader()\n }\n\n async function download(\n liveUrl: string,\n user: string,\n outputDir: string,\n maxDuration = 0,\n onProgress?: (info: ProgressInfo) => void,\n getNextUrl?: () => Promise<string | null>,\n ): Promise<DownloadResult> {\n abortFlag = false\n ensureDir(outputDir)\n\n const filename = formatFilename(user, 'flv')\n const filepath = join(outputDir, filename)\n logger?.info(`Recording: ${filename}`)\n\n const startTime = Date.now()\n let totalBytes = 0\n let lastProgressTime = 0\n\n let reader: ReadableStreamDefaultReader<Uint8Array>\n try {\n reader = await openReader(liveUrl)\n } catch (err) {\n throw new Error(`Failed to open stream: ${err instanceof Error ? err.message : String(err)}`)\n }\n streamReader = reader\n\n const writer = createWriteStream(filepath)\n\n /** Shared across try (read loop) and catch (error handler). */\n function elapsed(): number {\n return (Date.now() - startTime) / 1000\n }\n\n try {\n const bufferSize = 512 * 1024 // 512 KB buffer\n let buffer = Buffer.alloc(0)\n\n function reportProgress(bytes: number): void {\n if (!onProgress) return\n const now = Date.now()\n const e = elapsed()\n const speed = e > 0 ? bytes / e : 0\n // Throttle to at most once per second to avoid flooding the UI\n if (now - lastProgressTime >= 1000) {\n lastProgressTime = now\n onProgress({ bytes, elapsed: e, speed })\n }\n }\n\n /** Try to reconnect to a fresh stream URL. Returns true if reconnected. */\n async function tryReconnect(): Promise<boolean> {\n if (abortFlag) return false\n if (!getNextUrl) return false\n if (maxDuration > 0 && elapsed() >= maxDuration) return false\n\n logger?.info('Stream ended, attempting reconnection...')\n const nextUrl = await getNextUrl()\n if (!nextUrl) {\n logger?.info('No new stream URL — recording finished')\n return false\n }\n\n logger?.info(`Reconnecting to new stream URL: ${nextUrl}`)\n try {\n // Cancel old reader before opening new connection\n reader.cancel().catch(() => {})\n reader = await openReader(nextUrl)\n streamReader = reader\n logger?.info('Reconnected successfully')\n return true\n } catch (err) {\n logger?.error(`Reconnection failed: ${err instanceof Error ? err.message : String(err)}`)\n return false\n }\n }\n\n while (!abortFlag) {\n let chunk: { done: boolean; value?: Uint8Array }\n\n try {\n const result = await timeout(reader.read(), 60_000, 'Stream read timed out')\n chunk = result\n } catch (err) {\n // Read timeout — the TCP connection may have dropped silently.\n // Give TikTok a fresh URL if the stream is still live.\n logger?.info('Read timed out, attempting reconnection...')\n if (await tryReconnect()) {\n continue\n }\n // No reconnection available — propagate the error so the caller\n // at least gets whatever bytes were buffered.\n throw err\n }\n\n if (chunk.done) {\n // TikTok stream segments are short-lived (30-60s).\n // When one ends, fetch a fresh URL and keep recording.\n if (await tryReconnect()) {\n continue\n }\n break // no reconnection → recording is truly done\n }\n\n // Accumulate into buffer. chunk.value is guaranteed defined when done=false.\n buffer = Buffer.concat([buffer, Buffer.from(chunk.value!)])\n\n // Flush when buffer is full\n if (buffer.length >= bufferSize) {\n await writeBuffer(writer, buffer)\n totalBytes += buffer.length\n buffer = Buffer.alloc(0)\n }\n\n // Check duration limit\n if (maxDuration > 0) {\n const e = elapsed()\n if (e >= maxDuration) {\n logger?.info(`Duration limit reached (${maxDuration}s)`)\n break\n }\n }\n\n // Report progress after each chunk (throttled to 1s internally)\n reportProgress(totalBytes)\n }\n\n // Flush remaining buffer and ensure all data is on disk\n if (buffer.length > 0) {\n await writeBuffer(writer, buffer)\n totalBytes += buffer.length\n }\n await new Promise<void>((resolve) => writer.end(resolve))\n\n const duration = elapsed()\n // Final progress report\n if (onProgress) {\n onProgress({\n bytes: totalBytes,\n elapsed: duration,\n speed: duration > 0 ? totalBytes / duration : 0,\n })\n }\n logger?.info(\n `Recording finished: ${filename} (${formatDuration(duration)}, ${bytesToHuman(totalBytes)})`,\n )\n\n return { file: filepath, duration, size: totalBytes }\n } catch (err) {\n abort()\n const duration = elapsed()\n logger?.error(`Recording error: ${err instanceof Error ? err.message : String(err)}`)\n return { file: filepath, duration, size: totalBytes }\n } finally {\n streamReader = null\n }\n }\n\n return { download, abort }\n}\n\nfunction writeBuffer(writer: WriteStream, buffer: Buffer): Promise<void> {\n return new Promise((resolve, reject) => {\n writer.write(buffer, (err) => {\n if (err) reject(err)\n else resolve()\n })\n })\n}\n\nfunction formatDuration(seconds: number): string {\n const m = Math.floor(seconds / 60)\n const s = Math.floor(seconds % 60)\n return `${m}m ${s}s`\n}\n\n/** Race a promise against a timeout. The losing promise is ignored (no unhandled rejection). */\nfunction timeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {\n return new Promise<T>((resolve, reject) => {\n const timer = setTimeout(() => reject(new Error(label)), ms)\n promise.then(\n (result) => {\n clearTimeout(timer)\n resolve(result)\n },\n (err) => {\n clearTimeout(timer)\n reject(err)\n },\n )\n })\n}\n"],"mappings":"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"}
|
package/dist/ui.mjs
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import { ICON_ERROR, ICON_INFO, ICON_SUCCESS, ICON_WARN, color, createSpinner } from "@zfadhli/koko-cli";
|
|
2
|
+
//#region src/ui.ts
|
|
3
|
+
/**
|
|
4
|
+
* Terminal Display Manager — beautiful console output using koko-cli's
|
|
5
|
+
* spinner, icons, and color utilities.
|
|
6
|
+
*
|
|
7
|
+
* This module owns the terminal and should be the only thing writing to
|
|
8
|
+
* stdout/stderr (the file logger handles persistent logs).
|
|
9
|
+
*/
|
|
10
|
+
function createDisplay() {
|
|
11
|
+
let activeSpinner = null;
|
|
12
|
+
/** Stop the currently active spinner (if any) without printing anything. */
|
|
13
|
+
function clearSpinner() {
|
|
14
|
+
if (activeSpinner?.isSpinning) activeSpinner.stop();
|
|
15
|
+
activeSpinner = null;
|
|
16
|
+
}
|
|
17
|
+
/** Stop the current spinner and print a one-liner with the given icon. */
|
|
18
|
+
function finalize(icon, text) {
|
|
19
|
+
clearSpinner();
|
|
20
|
+
process.stdout.write(` ${icon} ${text}\n`);
|
|
21
|
+
}
|
|
22
|
+
/** Format seconds into a compact human-readable duration. */
|
|
23
|
+
function fmtDuration(seconds) {
|
|
24
|
+
const m = Math.floor(seconds / 60);
|
|
25
|
+
const s = Math.floor(seconds % 60);
|
|
26
|
+
if (m > 0) return `${m}m ${s}s`;
|
|
27
|
+
return `${s}s`;
|
|
28
|
+
}
|
|
29
|
+
/** Format bytes into human-readable size. */
|
|
30
|
+
function fmtBytes(bytes) {
|
|
31
|
+
if (bytes === 0) return "0 B";
|
|
32
|
+
const units = [
|
|
33
|
+
"B",
|
|
34
|
+
"KB",
|
|
35
|
+
"MB",
|
|
36
|
+
"GB"
|
|
37
|
+
];
|
|
38
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
39
|
+
return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`;
|
|
40
|
+
}
|
|
41
|
+
/** Format bytes per second into a readable speed string. */
|
|
42
|
+
function fmtSpeed(bytesPerSec) {
|
|
43
|
+
if (bytesPerSec < 1024) return `${bytesPerSec.toFixed(0)} B/s`;
|
|
44
|
+
if (bytesPerSec < 1024 * 1024) return `${(bytesPerSec / 1024).toFixed(1)} KB/s`;
|
|
45
|
+
return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s`;
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
pollingStarted(intervalMinutes) {
|
|
49
|
+
finalize(ICON_SUCCESS, color.dim(`Polling started (every ${intervalMinutes} min)`));
|
|
50
|
+
},
|
|
51
|
+
checkingUser(user) {
|
|
52
|
+
clearSpinner();
|
|
53
|
+
activeSpinner = createSpinner(color.cyan(`Checking @${user}...`));
|
|
54
|
+
activeSpinner.start();
|
|
55
|
+
},
|
|
56
|
+
userOffline(user) {
|
|
57
|
+
finalize(ICON_INFO, color.dim(`@${user} is offline`));
|
|
58
|
+
},
|
|
59
|
+
userLive(user, roomId) {
|
|
60
|
+
clearSpinner();
|
|
61
|
+
process.stdout.write(` ${ICON_SUCCESS} ${color.green(`@${user} is LIVE!`)} ${color.dim(`(room: ${roomId})`)}\n`);
|
|
62
|
+
},
|
|
63
|
+
startRecording() {
|
|
64
|
+
clearSpinner();
|
|
65
|
+
activeSpinner = createSpinner(color.cyan("Recording stream..."));
|
|
66
|
+
activeSpinner.start();
|
|
67
|
+
},
|
|
68
|
+
updateProgress(bytes, elapsed, speed) {
|
|
69
|
+
if (!activeSpinner || !activeSpinner.isSpinning) return;
|
|
70
|
+
activeSpinner.text = ` ${fmtBytes(bytes)} ${color.dim("•")} ${fmtDuration(elapsed)} ${color.dim("•")} ${fmtSpeed(speed)}`;
|
|
71
|
+
},
|
|
72
|
+
finishRecording(filename, duration, size) {
|
|
73
|
+
finalize(ICON_SUCCESS, `${color.green("Recording finished")} ${color.dim(`${filename} — ${size} in ${fmtDuration(duration)}`)}`);
|
|
74
|
+
},
|
|
75
|
+
recordingError(message) {
|
|
76
|
+
finalize(ICON_ERROR, color.red(`Recording failed: ${message}`));
|
|
77
|
+
},
|
|
78
|
+
startSegmenting() {
|
|
79
|
+
clearSpinner();
|
|
80
|
+
activeSpinner = createSpinner(color.cyan("Segmenting..."));
|
|
81
|
+
activeSpinner.start();
|
|
82
|
+
},
|
|
83
|
+
segmentsCreated(count) {
|
|
84
|
+
finalize(ICON_SUCCESS, color.green(`Created ${count} MP4 segment${count !== 1 ? "s" : ""}`));
|
|
85
|
+
},
|
|
86
|
+
startConverting() {
|
|
87
|
+
clearSpinner();
|
|
88
|
+
activeSpinner = createSpinner(color.cyan("Converting to MP4..."));
|
|
89
|
+
activeSpinner.start();
|
|
90
|
+
},
|
|
91
|
+
conversionDone(output) {
|
|
92
|
+
finalize(ICON_SUCCESS, color.green(`Converted: ${output}`));
|
|
93
|
+
},
|
|
94
|
+
showError(message) {
|
|
95
|
+
clearSpinner();
|
|
96
|
+
process.stdout.write(` ${ICON_ERROR} ${color.red(message)}\n`);
|
|
97
|
+
},
|
|
98
|
+
showInfo(message) {
|
|
99
|
+
clearSpinner();
|
|
100
|
+
process.stdout.write(` ${ICON_INFO} ${color.dim(message)}\n`);
|
|
101
|
+
},
|
|
102
|
+
showWarning(message) {
|
|
103
|
+
clearSpinner();
|
|
104
|
+
process.stdout.write(` ${ICON_WARN} ${color.yellow(message)}\n`);
|
|
105
|
+
},
|
|
106
|
+
stop() {
|
|
107
|
+
clearSpinner();
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
//#endregion
|
|
112
|
+
export { createDisplay };
|
|
113
|
+
|
|
114
|
+
//# sourceMappingURL=ui.mjs.map
|
package/dist/ui.mjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ui.mjs","names":[],"sources":["../src/ui.ts"],"sourcesContent":["/**\n * Terminal Display Manager — beautiful console output using koko-cli's\n * spinner, icons, and color utilities.\n *\n * This module owns the terminal and should be the only thing writing to\n * stdout/stderr (the file logger handles persistent logs).\n */\n\nimport {\n ICON_ERROR,\n ICON_INFO,\n ICON_SUCCESS,\n ICON_WARN,\n color,\n createSpinner,\n} from '@zfadhli/koko-cli'\nimport type { SpinnerInstance } from '@zfadhli/koko-cli'\n\nexport interface Display {\n /** Called once when polling begins. */\n pollingStarted(intervalMinutes: number): void\n /** Spinner while checking if a user is live. */\n checkingUser(user: string): void\n /** User is offline (no active display). */\n userOffline(user: string): void\n /** User is live! Clears the checking spinner. */\n userLive(user: string, roomId: string): void\n\n /** Recording starts: spinner without filename (known only post-download). */\n startRecording(): void\n /** Update the recording spinner with live counters. */\n updateProgress(bytes: number, elapsed: number, speed: number): void\n /** Recording finished successfully. Shows filename from the result. */\n finishRecording(filename: string, duration: number, size: string): void\n /** Recording encountered a non-fatal error. */\n recordingError(message: string): void\n\n /** FFmpeg segmenting starts. */\n startSegmenting(): void\n /** FFmpeg segmenting done. */\n segmentsCreated(count: number): void\n\n /** FFmpeg simple conversion starts (fallback path). */\n startConverting(): void\n /** Simple conversion done. */\n conversionDone(output: string): void\n\n /** General-purpose status helpers. */\n showError(message: string): void\n showInfo(message: string): void\n showWarning(message: string): void\n\n /** Stop all active spinners and clean up the terminal. */\n stop(): void\n}\n\nexport function createDisplay(): Display {\n let activeSpinner: SpinnerInstance | null = null\n\n /** Stop the currently active spinner (if any) without printing anything. */\n function clearSpinner(): void {\n if (activeSpinner?.isSpinning) {\n activeSpinner.stop()\n }\n activeSpinner = null\n }\n\n /** Stop the current spinner and print a one-liner with the given icon. */\n function finalize(icon: string, text: string): void {\n clearSpinner()\n process.stdout.write(` ${icon} ${text}\\n`)\n }\n\n /** Format seconds into a compact human-readable duration. */\n function fmtDuration(seconds: number): string {\n const m = Math.floor(seconds / 60)\n const s = Math.floor(seconds % 60)\n if (m > 0) return `${m}m ${s}s`\n return `${s}s`\n }\n\n /** Format bytes into human-readable size. */\n function fmtBytes(bytes: number): string {\n if (bytes === 0) return '0 B'\n const units = ['B', 'KB', 'MB', 'GB']\n const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)\n return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`\n }\n\n /** Format bytes per second into a readable speed string. */\n function fmtSpeed(bytesPerSec: number): string {\n if (bytesPerSec < 1024) return `${bytesPerSec.toFixed(0)} B/s`\n if (bytesPerSec < 1024 * 1024) return `${(bytesPerSec / 1024).toFixed(1)} KB/s`\n return `${(bytesPerSec / (1024 * 1024)).toFixed(1)} MB/s`\n }\n\n return {\n pollingStarted(intervalMinutes: number): void {\n finalize(ICON_SUCCESS, color.dim(`Polling started (every ${intervalMinutes} min)`))\n },\n\n checkingUser(user: string): void {\n clearSpinner()\n activeSpinner = createSpinner(color.cyan(`Checking @${user}...`))\n activeSpinner.start()\n },\n\n userOffline(user: string): void {\n finalize(ICON_INFO, color.dim(`@${user} is offline`))\n },\n\n userLive(user: string, roomId: string): void {\n clearSpinner()\n process.stdout.write(\n ` ${ICON_SUCCESS} ${color.green(`@${user} is LIVE!`)} ${color.dim(`(room: ${roomId})`)}\\n`,\n )\n },\n\n startRecording(): void {\n clearSpinner()\n activeSpinner = createSpinner(color.cyan('Recording stream...'))\n activeSpinner.start()\n },\n\n updateProgress(bytes: number, elapsed: number, speed: number): void {\n if (!activeSpinner || !activeSpinner.isSpinning) return\n activeSpinner.text = ` ${fmtBytes(bytes)} ${color.dim('•')} ${fmtDuration(elapsed)} ${color.dim('•')} ${fmtSpeed(speed)}`\n },\n\n finishRecording(filename: string, duration: number, size: string): void {\n finalize(\n ICON_SUCCESS,\n `${color.green('Recording finished')} ${color.dim(`${filename} — ${size} in ${fmtDuration(duration)}`)}`,\n )\n },\n\n recordingError(message: string): void {\n finalize(ICON_ERROR, color.red(`Recording failed: ${message}`))\n },\n\n startSegmenting(): void {\n clearSpinner()\n activeSpinner = createSpinner(color.cyan('Segmenting...'))\n activeSpinner.start()\n },\n\n segmentsCreated(count: number): void {\n finalize(ICON_SUCCESS, color.green(`Created ${count} MP4 segment${count !== 1 ? 's' : ''}`))\n },\n\n startConverting(): void {\n clearSpinner()\n activeSpinner = createSpinner(color.cyan('Converting to MP4...'))\n activeSpinner.start()\n },\n\n conversionDone(output: string): void {\n finalize(ICON_SUCCESS, color.green(`Converted: ${output}`))\n },\n\n showError(message: string): void {\n clearSpinner()\n process.stdout.write(` ${ICON_ERROR} ${color.red(message)}\\n`)\n },\n\n showInfo(message: string): void {\n clearSpinner()\n process.stdout.write(` ${ICON_INFO} ${color.dim(message)}\\n`)\n },\n\n showWarning(message: string): void {\n clearSpinner()\n process.stdout.write(` ${ICON_WARN} ${color.yellow(message)}\\n`)\n },\n\n stop(): void {\n clearSpinner()\n },\n }\n}\n"],"mappings":";;;;;;;;;AAwDA,SAAgB,gBAAyB;CACvC,IAAI,gBAAwC;;CAG5C,SAAS,eAAqB;EAC5B,IAAI,eAAe,YACjB,cAAc,KAAK;EAErB,gBAAgB;CAClB;;CAGA,SAAS,SAAS,MAAc,MAAoB;EAClD,aAAa;EACb,QAAQ,OAAO,MAAM,KAAK,KAAK,GAAG,KAAK,GAAG;CAC5C;;CAGA,SAAS,YAAY,SAAyB;EAC5C,MAAM,IAAI,KAAK,MAAM,UAAU,EAAE;EACjC,MAAM,IAAI,KAAK,MAAM,UAAU,EAAE;EACjC,IAAI,IAAI,GAAG,OAAO,GAAG,EAAE,IAAI,EAAE;EAC7B,OAAO,GAAG,EAAE;CACd;;CAGA,SAAS,SAAS,OAAuB;EACvC,IAAI,UAAU,GAAG,OAAO;EACxB,MAAM,QAAQ;GAAC;GAAK;GAAM;GAAM;EAAI;EACpC,MAAM,IAAI,KAAK,IAAI,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,IAAI,CAAC,GAAG,MAAM,SAAS,CAAC;EACjF,OAAO,IAAI,QAAQ,QAAQ,EAAA,CAAG,QAAQ,CAAC,EAAE,GAAG,MAAM;CACpD;;CAGA,SAAS,SAAS,aAA6B;EAC7C,IAAI,cAAc,MAAM,OAAO,GAAG,YAAY,QAAQ,CAAC,EAAE;EACzD,IAAI,cAAc,OAAO,MAAM,OAAO,IAAI,cAAc,KAAA,CAAM,QAAQ,CAAC,EAAE;EACzE,OAAO,IAAI,eAAe,OAAO,MAAA,CAAO,QAAQ,CAAC,EAAE;CACrD;CAEA,OAAO;EACL,eAAe,iBAA+B;GAC5C,SAAS,cAAc,MAAM,IAAI,0BAA0B,gBAAgB,MAAM,CAAC;EACpF;EAEA,aAAa,MAAoB;GAC/B,aAAa;GACb,gBAAgB,cAAc,MAAM,KAAK,aAAa,KAAK,IAAI,CAAC;GAChE,cAAc,MAAM;EACtB;EAEA,YAAY,MAAoB;GAC9B,SAAS,WAAW,MAAM,IAAI,IAAI,KAAK,YAAY,CAAC;EACtD;EAEA,SAAS,MAAc,QAAsB;GAC3C,aAAa;GACb,QAAQ,OAAO,MACb,KAAK,aAAa,GAAG,MAAM,MAAM,IAAI,KAAK,UAAU,EAAE,GAAG,MAAM,IAAI,UAAU,OAAO,EAAE,EAAE,GAC1F;EACF;EAEA,iBAAuB;GACrB,aAAa;GACb,gBAAgB,cAAc,MAAM,KAAK,qBAAqB,CAAC;GAC/D,cAAc,MAAM;EACtB;EAEA,eAAe,OAAe,SAAiB,OAAqB;GAClE,IAAI,CAAC,iBAAiB,CAAC,cAAc,YAAY;GACjD,cAAc,OAAO,IAAI,SAAS,KAAK,EAAE,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,YAAY,OAAO,EAAE,GAAG,MAAM,IAAI,GAAG,EAAE,GAAG,SAAS,KAAK;EACxH;EAEA,gBAAgB,UAAkB,UAAkB,MAAoB;GACtE,SACE,cACA,GAAG,MAAM,MAAM,oBAAoB,EAAE,GAAG,MAAM,IAAI,GAAG,SAAS,KAAK,KAAK,MAAM,YAAY,QAAQ,GAAG,GACvG;EACF;EAEA,eAAe,SAAuB;GACpC,SAAS,YAAY,MAAM,IAAI,qBAAqB,SAAS,CAAC;EAChE;EAEA,kBAAwB;GACtB,aAAa;GACb,gBAAgB,cAAc,MAAM,KAAK,eAAe,CAAC;GACzD,cAAc,MAAM;EACtB;EAEA,gBAAgB,OAAqB;GACnC,SAAS,cAAc,MAAM,MAAM,WAAW,MAAM,cAAc,UAAU,IAAI,MAAM,IAAI,CAAC;EAC7F;EAEA,kBAAwB;GACtB,aAAa;GACb,gBAAgB,cAAc,MAAM,KAAK,sBAAsB,CAAC;GAChE,cAAc,MAAM;EACtB;EAEA,eAAe,QAAsB;GACnC,SAAS,cAAc,MAAM,MAAM,cAAc,QAAQ,CAAC;EAC5D;EAEA,UAAU,SAAuB;GAC/B,aAAa;GACb,QAAQ,OAAO,MAAM,KAAK,WAAW,GAAG,MAAM,IAAI,OAAO,EAAE,GAAG;EAChE;EAEA,SAAS,SAAuB;GAC9B,aAAa;GACb,QAAQ,OAAO,MAAM,KAAK,UAAU,GAAG,MAAM,IAAI,OAAO,EAAE,GAAG;EAC/D;EAEA,YAAY,SAAuB;GACjC,aAAa;GACb,QAAQ,OAAO,MAAM,KAAK,UAAU,GAAG,MAAM,OAAO,OAAO,EAAE,GAAG;EAClE;EAEA,OAAa;GACX,aAAa;EACf;CACF;AACF"}
|
package/dist/utils.mjs
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { mkdirSync } from "node:fs";
|
|
2
|
+
//#region src/utils.ts
|
|
3
|
+
/**
|
|
4
|
+
* Utility helpers — pure functions, no side effects.
|
|
5
|
+
*/
|
|
6
|
+
/** Format a filename like username=2025.06.09_14-30-00_1.flv */
|
|
7
|
+
function formatFilename(user, ext = "flv", part) {
|
|
8
|
+
const now = /* @__PURE__ */ new Date();
|
|
9
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
10
|
+
return `${user}=${`${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`}_${`${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`}${part !== void 0 ? `_part${part}` : ""}.${ext}`;
|
|
11
|
+
}
|
|
12
|
+
/** Ensure a directory exists (recursive) */
|
|
13
|
+
function ensureDir(dir) {
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
/** Sleep for ms milliseconds */
|
|
17
|
+
function sleep(ms) {
|
|
18
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
19
|
+
}
|
|
20
|
+
/** Format bytes to human-readable size */
|
|
21
|
+
function bytesToHuman(bytes) {
|
|
22
|
+
if (bytes === 0) return "0 B";
|
|
23
|
+
const units = [
|
|
24
|
+
"B",
|
|
25
|
+
"KB",
|
|
26
|
+
"MB",
|
|
27
|
+
"GB"
|
|
28
|
+
];
|
|
29
|
+
const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1);
|
|
30
|
+
return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`;
|
|
31
|
+
}
|
|
32
|
+
/** Strip leading @ from a username */
|
|
33
|
+
function sanitizeUser(user) {
|
|
34
|
+
return user.replace(/^@+/, "").trim();
|
|
35
|
+
}
|
|
36
|
+
//#endregion
|
|
37
|
+
export { bytesToHuman, ensureDir, formatFilename, sanitizeUser, sleep };
|
|
38
|
+
|
|
39
|
+
//# sourceMappingURL=utils.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.mjs","names":[],"sources":["../src/utils.ts"],"sourcesContent":["/**\n * Utility helpers — pure functions, no side effects.\n */\n\nimport { mkdirSync } from 'node:fs'\n\n/** Format a filename like username=2025.06.09_14-30-00_1.flv */\nexport function formatFilename(user: string, ext = 'flv', part?: number): string {\n const now = new Date()\n const pad = (n: number) => String(n).padStart(2, '0')\n const date = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}`\n const time = `${pad(now.getHours())}${pad(now.getMinutes())}${pad(now.getSeconds())}`\n const partSuffix = part !== undefined ? `_part${part}` : ''\n return `${user}=${date}_${time}${partSuffix}.${ext}`\n}\n\n/** Ensure a directory exists (recursive) */\nexport function ensureDir(dir: string): void {\n mkdirSync(dir, { recursive: true })\n}\n\n/** Sleep for ms milliseconds */\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms))\n}\n\n/** Format bytes to human-readable size */\nexport function bytesToHuman(bytes: number): string {\n if (bytes === 0) return '0 B'\n const units = ['B', 'KB', 'MB', 'GB']\n const i = Math.min(Math.floor(Math.log(bytes) / Math.log(1024)), units.length - 1)\n return `${(bytes / 1024 ** i).toFixed(1)} ${units[i]}`\n}\n\n/** Strip leading @ from a username */\nexport function sanitizeUser(user: string): string {\n return user.replace(/^@+/, '').trim()\n}\n"],"mappings":";;;;;;AAOA,SAAgB,eAAe,MAAc,MAAM,OAAO,MAAuB;CAC/E,MAAM,sBAAM,IAAI,KAAK;CACrB,MAAM,OAAO,MAAc,OAAO,CAAC,CAAC,CAAC,SAAS,GAAG,GAAG;CAIpD,OAAO,GAAG,KAAK,GAAG,GAHF,IAAI,YAAY,IAAI,IAAI,IAAI,SAAS,IAAI,CAAC,IAAI,IAAI,IAAI,QAAQ,CAAC,IAGxD,GAAG,GAFV,IAAI,IAAI,SAAS,CAAC,IAAI,IAAI,IAAI,WAAW,CAAC,IAAI,IAAI,IAAI,WAAW,CAAC,MAC/D,SAAS,KAAA,IAAY,QAAQ,SAAS,GACb,GAAG;AACjD;;AAGA,SAAgB,UAAU,KAAmB;CAC3C,UAAU,KAAK,EAAE,WAAW,KAAK,CAAC;AACpC;;AAGA,SAAgB,MAAM,IAA2B;CAC/C,OAAO,IAAI,SAAS,YAAY,WAAW,SAAS,EAAE,CAAC;AACzD;;AAGA,SAAgB,aAAa,OAAuB;CAClD,IAAI,UAAU,GAAG,OAAO;CACxB,MAAM,QAAQ;EAAC;EAAK;EAAM;EAAM;CAAI;CACpC,MAAM,IAAI,KAAK,IAAI,KAAK,MAAM,KAAK,IAAI,KAAK,IAAI,KAAK,IAAI,IAAI,CAAC,GAAG,MAAM,SAAS,CAAC;CACjF,OAAO,IAAI,QAAQ,QAAQ,EAAA,CAAG,QAAQ,CAAC,EAAE,GAAG,MAAM;AACpD;;AAGA,SAAgB,aAAa,MAAsB;CACjD,OAAO,KAAK,QAAQ,OAAO,EAAE,CAAC,CAAC,KAAK;AACtC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zfadhli/tokrec",
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Minimal TikTok live stream recorder — Bun + TypeScript",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/lib.mjs",
|
|
7
|
+
"types": "./dist/lib.d.mts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/lib.d.mts",
|
|
11
|
+
"default": "./dist/lib.mjs"
|
|
12
|
+
},
|
|
13
|
+
"./cli": {
|
|
14
|
+
"types": "./dist/index.d.mts",
|
|
15
|
+
"default": "./dist/index.mjs"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"bin": {
|
|
19
|
+
"tiktok-live-recorder": "./dist/index.mjs"
|
|
20
|
+
},
|
|
21
|
+
"files": [
|
|
22
|
+
"dist"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"dev": "bun run src/index.ts",
|
|
26
|
+
"build": "tsdown",
|
|
27
|
+
"typecheck": "tsc --noEmit",
|
|
28
|
+
"test": "bun test",
|
|
29
|
+
"lint": "biome check src/",
|
|
30
|
+
"lint:fix": "biome check --write src/",
|
|
31
|
+
"format": "biome format --write src/"
|
|
32
|
+
},
|
|
33
|
+
"dependencies": {
|
|
34
|
+
"@zfadhli/koko-cli": "^0.1.0",
|
|
35
|
+
"wreq-js": "^2.3"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@biomejs/biome": "^1.9",
|
|
39
|
+
"@types/bun": "latest",
|
|
40
|
+
"tsdown": "^0.22",
|
|
41
|
+
"typescript": "^5.8"
|
|
42
|
+
},
|
|
43
|
+
"engines": {
|
|
44
|
+
"bun": ">=1.2"
|
|
45
|
+
},
|
|
46
|
+
"license": "MIT",
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/zfadhli/tokrec.git"
|
|
50
|
+
},
|
|
51
|
+
"bugs": {
|
|
52
|
+
"url": "https://github.com/zfadhli/tokrec/issues"
|
|
53
|
+
},
|
|
54
|
+
"homepage": "https://github.com/zfadhli/tokrec",
|
|
55
|
+
"publishConfig": {
|
|
56
|
+
"access": "public"
|
|
57
|
+
}
|
|
58
|
+
}
|