tokwatchr 0.4.3 → 0.5.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/index.d.mts +17 -1
- package/dist/index.mjs +41 -3
- package/package.json +1 -1
- package/src/TikTokLiveDownloader.ts +56 -4
- package/src/index.ts +1 -0
- package/src/types.ts +11 -0
package/dist/index.d.mts
CHANGED
|
@@ -88,7 +88,17 @@ interface CookieJarLike {
|
|
|
88
88
|
setCookie(raw: string, url: string): Promise<void>;
|
|
89
89
|
getCookieString(url: string): Promise<string>;
|
|
90
90
|
}
|
|
91
|
+
interface WaitingInfo {
|
|
92
|
+
/** The username being waited on. */
|
|
93
|
+
username: string;
|
|
94
|
+
/** Which phase we're waiting in: "room" = looking for a room, "stream" = stream not active. */
|
|
95
|
+
phase: "room" | "stream";
|
|
96
|
+
/** Seconds elapsed since this phase started. */
|
|
97
|
+
elapsed: number;
|
|
98
|
+
}
|
|
91
99
|
interface TikTokLiveDownloaderEvents {
|
|
100
|
+
/** Emitted periodically while waiting for a room or stream to become active. */
|
|
101
|
+
waiting: [info: WaitingInfo];
|
|
92
102
|
/** Emitted when a live stream is detected and we're about to start recording. */
|
|
93
103
|
start: [info: StreamInfo];
|
|
94
104
|
/** Emitted periodically (every ~1s) during recording with current stats. */
|
|
@@ -237,6 +247,12 @@ declare class TikTokLiveDownloader {
|
|
|
237
247
|
private resolveRoomIdOnce;
|
|
238
248
|
private resolveRoomIdWithRetry;
|
|
239
249
|
private fetchStreamInfo;
|
|
250
|
+
/**
|
|
251
|
+
* Poll for stream info until the stream becomes active.
|
|
252
|
+
* Used in waitForLive mode when the room exists but the stream
|
|
253
|
+
* has not yet started broadcasting.
|
|
254
|
+
*/
|
|
255
|
+
private pollStreamInfo;
|
|
240
256
|
/**
|
|
241
257
|
* Download a single segment. Always downloads to .ts (crash-safe).
|
|
242
258
|
* Does NOT remux — that is handled by remuxSegment() running in the background.
|
|
@@ -324,4 +340,4 @@ declare function renderFilename(template: string, values: {
|
|
|
324
340
|
part?: number;
|
|
325
341
|
}): string;
|
|
326
342
|
//#endregion
|
|
327
|
-
export { AbortError, type CookieJarLike, type CreateClientOptions, DownloadFailedError, type DownloadFunctionOptions, type DownloadResult, type DownloadStats, type DownloaderState, FfmpegError, type OutputFormat, type QualityOption, RoomResolveError, type RoomResolveOptions, StreamFetchError, type StreamInfo, type StreamInfoOptions, type StreamQualityKey, TikTokLiveDownloader, type TikTokLiveDownloaderEvents, type TikTokLiveDownloaderOptions, TikTokLiveError, UserNotFoundError, UserOfflineError, buildQualityOption, createClient, download, fetchStreamInfo, parseQualities, renderFilename, resolveRoomId, selectQuality };
|
|
343
|
+
export { AbortError, type CookieJarLike, type CreateClientOptions, DownloadFailedError, type DownloadFunctionOptions, type DownloadResult, type DownloadStats, type DownloaderState, FfmpegError, type OutputFormat, type QualityOption, RoomResolveError, type RoomResolveOptions, StreamFetchError, type StreamInfo, type StreamInfoOptions, type StreamQualityKey, TikTokLiveDownloader, type TikTokLiveDownloaderEvents, type TikTokLiveDownloaderOptions, TikTokLiveError, UserNotFoundError, UserOfflineError, type WaitingInfo, buildQualityOption, createClient, download, fetchStreamInfo, parseQualities, renderFilename, resolveRoomId, selectQuality };
|
package/dist/index.mjs
CHANGED
|
@@ -620,7 +620,7 @@ const DEFAULT_OPTIONS = {
|
|
|
620
620
|
bitrate: null,
|
|
621
621
|
maxDuration: Infinity,
|
|
622
622
|
maxSegmentDuration: Infinity,
|
|
623
|
-
checkInterval:
|
|
623
|
+
checkInterval: 18e4,
|
|
624
624
|
proxyUrl: null,
|
|
625
625
|
cookieJar: null,
|
|
626
626
|
browser: "chrome",
|
|
@@ -773,7 +773,7 @@ var TikTokLiveDownloader = class {
|
|
|
773
773
|
if (!roomId && waitForLive) roomId = await this.resolveRoomIdWithRetry();
|
|
774
774
|
if (!roomId) throw new UserOfflineError(this.username);
|
|
775
775
|
this.setState("waiting");
|
|
776
|
-
const firstInfo = await this.fetchStreamInfo(roomId);
|
|
776
|
+
const firstInfo = waitForLive ? await this.pollStreamInfo(roomId) : await this.fetchStreamInfo(roomId);
|
|
777
777
|
if (!(this.options.maxSegmentDuration > 0 && this.options.maxSegmentDuration < Infinity)) {
|
|
778
778
|
const tsResult = await this.downloadSegment(firstInfo, 1);
|
|
779
779
|
const finalResult = await this.remuxSegment(tsResult);
|
|
@@ -808,6 +808,11 @@ var TikTokLiveDownloader = class {
|
|
|
808
808
|
this.emit("complete", finalResults);
|
|
809
809
|
return finalResults[finalResults.length - 1];
|
|
810
810
|
} catch (err) {
|
|
811
|
+
if (err instanceof StreamFetchError) this.emit("waiting", {
|
|
812
|
+
username: this.username,
|
|
813
|
+
phase: "stream",
|
|
814
|
+
elapsed: 0
|
|
815
|
+
});
|
|
811
816
|
if (pendingRemuxes.length > 0) await Promise.allSettled(pendingRemuxes);
|
|
812
817
|
this.setState("done");
|
|
813
818
|
const error = err instanceof Error ? err : new Error(String(err));
|
|
@@ -834,11 +839,17 @@ var TikTokLiveDownloader = class {
|
|
|
834
839
|
}
|
|
835
840
|
async resolveRoomIdWithRetry() {
|
|
836
841
|
const interval = this.options.checkInterval;
|
|
837
|
-
const maxInterval = Math.max(interval,
|
|
842
|
+
const maxInterval = Math.max(interval, 18e4);
|
|
843
|
+
const waitStart = Date.now();
|
|
838
844
|
while (true) {
|
|
839
845
|
this.abortController.signal?.throwIfAborted();
|
|
840
846
|
const roomId = await this.resolveRoomIdOnce();
|
|
841
847
|
if (roomId) return roomId;
|
|
848
|
+
this.emit("waiting", {
|
|
849
|
+
username: this.username,
|
|
850
|
+
phase: "room",
|
|
851
|
+
elapsed: (Date.now() - waitStart) / 1e3
|
|
852
|
+
});
|
|
842
853
|
await sleep(maxInterval);
|
|
843
854
|
}
|
|
844
855
|
}
|
|
@@ -852,6 +863,33 @@ var TikTokLiveDownloader = class {
|
|
|
852
863
|
return info;
|
|
853
864
|
}
|
|
854
865
|
/**
|
|
866
|
+
* Poll for stream info until the stream becomes active.
|
|
867
|
+
* Used in waitForLive mode when the room exists but the stream
|
|
868
|
+
* has not yet started broadcasting.
|
|
869
|
+
*/
|
|
870
|
+
async pollStreamInfo(roomId) {
|
|
871
|
+
const interval = this.options.checkInterval;
|
|
872
|
+
const maxInterval = Math.max(interval, 18e4);
|
|
873
|
+
const waitStart = Date.now();
|
|
874
|
+
while (true) {
|
|
875
|
+
this.abortController.signal?.throwIfAborted();
|
|
876
|
+
try {
|
|
877
|
+
return await this.fetchStreamInfo(roomId);
|
|
878
|
+
} catch (err) {
|
|
879
|
+
if (err instanceof StreamFetchError) {
|
|
880
|
+
this.emit("waiting", {
|
|
881
|
+
username: this.username,
|
|
882
|
+
phase: "stream",
|
|
883
|
+
elapsed: (Date.now() - waitStart) / 1e3
|
|
884
|
+
});
|
|
885
|
+
await sleep(maxInterval);
|
|
886
|
+
continue;
|
|
887
|
+
}
|
|
888
|
+
throw err;
|
|
889
|
+
}
|
|
890
|
+
}
|
|
891
|
+
}
|
|
892
|
+
/**
|
|
855
893
|
* Download a single segment. Always downloads to .ts (crash-safe).
|
|
856
894
|
* Does NOT remux — that is handled by remuxSegment() running in the background.
|
|
857
895
|
*/
|
package/package.json
CHANGED
|
@@ -9,7 +9,11 @@ import { resolveRoomId } from "./api/room.js";
|
|
|
9
9
|
import { fetchStreamInfo } from "./api/stream.js";
|
|
10
10
|
import { downloadWithFfmpeg } from "./download/ffmpeg.js";
|
|
11
11
|
import { downloadRawHttp } from "./download/raw-http.js";
|
|
12
|
-
import {
|
|
12
|
+
import {
|
|
13
|
+
StreamFetchError,
|
|
14
|
+
UserNotFoundError,
|
|
15
|
+
UserOfflineError,
|
|
16
|
+
} from "./errors.js";
|
|
13
17
|
import type {
|
|
14
18
|
DownloaderState,
|
|
15
19
|
DownloadResult,
|
|
@@ -32,7 +36,7 @@ const DEFAULT_OPTIONS: ResolvedOptions = {
|
|
|
32
36
|
bitrate: null,
|
|
33
37
|
maxDuration: Infinity,
|
|
34
38
|
maxSegmentDuration: Infinity,
|
|
35
|
-
checkInterval:
|
|
39
|
+
checkInterval: 180_000,
|
|
36
40
|
proxyUrl: null,
|
|
37
41
|
cookieJar: null,
|
|
38
42
|
browser: "chrome",
|
|
@@ -259,7 +263,9 @@ export class TikTokLiveDownloader {
|
|
|
259
263
|
|
|
260
264
|
// Phase 2: Get stream info (refreshed per segment to avoid stale URLs)
|
|
261
265
|
this.setState("waiting");
|
|
262
|
-
const firstInfo =
|
|
266
|
+
const firstInfo = waitForLive
|
|
267
|
+
? await this.pollStreamInfo(roomId)
|
|
268
|
+
: await this.fetchStreamInfo(roomId);
|
|
263
269
|
|
|
264
270
|
const segmentEnabled =
|
|
265
271
|
this.options.maxSegmentDuration > 0 &&
|
|
@@ -314,6 +320,16 @@ export class TikTokLiveDownloader {
|
|
|
314
320
|
// biome-ignore lint/style/noNonNullAssertion: at least one result
|
|
315
321
|
return finalResults[finalResults.length - 1]!;
|
|
316
322
|
} catch (err) {
|
|
323
|
+
// One-shot waiting feedback for startRecording() users
|
|
324
|
+
// when the stream isn't active yet.
|
|
325
|
+
if (err instanceof StreamFetchError) {
|
|
326
|
+
this.emit("waiting", {
|
|
327
|
+
username: this.username,
|
|
328
|
+
phase: "stream",
|
|
329
|
+
elapsed: 0,
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
317
333
|
// Await any background remuxes before shutting down,
|
|
318
334
|
// so they can finish producing .mp4 and delete temp .ts files.
|
|
319
335
|
if (pendingRemuxes.length > 0) {
|
|
@@ -361,7 +377,8 @@ export class TikTokLiveDownloader {
|
|
|
361
377
|
|
|
362
378
|
private async resolveRoomIdWithRetry(): Promise<string> {
|
|
363
379
|
const interval = this.options.checkInterval;
|
|
364
|
-
const maxInterval = Math.max(interval,
|
|
380
|
+
const maxInterval = Math.max(interval, 180_000);
|
|
381
|
+
const waitStart = Date.now();
|
|
365
382
|
|
|
366
383
|
while (true) {
|
|
367
384
|
this.abortController.signal?.throwIfAborted();
|
|
@@ -371,6 +388,11 @@ export class TikTokLiveDownloader {
|
|
|
371
388
|
return roomId;
|
|
372
389
|
}
|
|
373
390
|
|
|
391
|
+
this.emit("waiting", {
|
|
392
|
+
username: this.username,
|
|
393
|
+
phase: "room",
|
|
394
|
+
elapsed: (Date.now() - waitStart) / 1_000,
|
|
395
|
+
});
|
|
374
396
|
await sleep(maxInterval);
|
|
375
397
|
}
|
|
376
398
|
}
|
|
@@ -387,6 +409,36 @@ export class TikTokLiveDownloader {
|
|
|
387
409
|
return info;
|
|
388
410
|
}
|
|
389
411
|
|
|
412
|
+
/**
|
|
413
|
+
* Poll for stream info until the stream becomes active.
|
|
414
|
+
* Used in waitForLive mode when the room exists but the stream
|
|
415
|
+
* has not yet started broadcasting.
|
|
416
|
+
*/
|
|
417
|
+
private async pollStreamInfo(roomId: string): Promise<StreamInfo> {
|
|
418
|
+
const interval = this.options.checkInterval;
|
|
419
|
+
const maxInterval = Math.max(interval, 180_000);
|
|
420
|
+
const waitStart = Date.now();
|
|
421
|
+
|
|
422
|
+
while (true) {
|
|
423
|
+
this.abortController.signal?.throwIfAborted();
|
|
424
|
+
|
|
425
|
+
try {
|
|
426
|
+
return await this.fetchStreamInfo(roomId);
|
|
427
|
+
} catch (err) {
|
|
428
|
+
if (err instanceof StreamFetchError) {
|
|
429
|
+
this.emit("waiting", {
|
|
430
|
+
username: this.username,
|
|
431
|
+
phase: "stream",
|
|
432
|
+
elapsed: (Date.now() - waitStart) / 1_000,
|
|
433
|
+
});
|
|
434
|
+
await sleep(maxInterval);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
throw err;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
390
442
|
/**
|
|
391
443
|
* Download a single segment. Always downloads to .ts (crash-safe).
|
|
392
444
|
* Does NOT remux — that is handled by remuxSegment() running in the background.
|
package/src/index.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -114,7 +114,18 @@ export interface CookieJarLike {
|
|
|
114
114
|
|
|
115
115
|
// ─── Events ───────────────────────────────────────────────
|
|
116
116
|
|
|
117
|
+
export interface WaitingInfo {
|
|
118
|
+
/** The username being waited on. */
|
|
119
|
+
username: string;
|
|
120
|
+
/** Which phase we're waiting in: "room" = looking for a room, "stream" = stream not active. */
|
|
121
|
+
phase: "room" | "stream";
|
|
122
|
+
/** Seconds elapsed since this phase started. */
|
|
123
|
+
elapsed: number;
|
|
124
|
+
}
|
|
125
|
+
|
|
117
126
|
export interface TikTokLiveDownloaderEvents {
|
|
127
|
+
/** Emitted periodically while waiting for a room or stream to become active. */
|
|
128
|
+
waiting: [info: WaitingInfo];
|
|
118
129
|
/** Emitted when a live stream is detected and we're about to start recording. */
|
|
119
130
|
start: [info: StreamInfo];
|
|
120
131
|
/** Emitted periodically (every ~1s) during recording with current stats. */
|