tokwatchr 0.4.4 → 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 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. */
@@ -330,4 +340,4 @@ declare function renderFilename(template: string, values: {
330
340
  part?: number;
331
341
  }): string;
332
342
  //#endregion
333
- 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
@@ -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));
@@ -835,10 +840,16 @@ var TikTokLiveDownloader = class {
835
840
  async resolveRoomIdWithRetry() {
836
841
  const interval = this.options.checkInterval;
837
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
  }
@@ -859,12 +870,18 @@ var TikTokLiveDownloader = class {
859
870
  async pollStreamInfo(roomId) {
860
871
  const interval = this.options.checkInterval;
861
872
  const maxInterval = Math.max(interval, 18e4);
873
+ const waitStart = Date.now();
862
874
  while (true) {
863
875
  this.abortController.signal?.throwIfAborted();
864
876
  try {
865
877
  return await this.fetchStreamInfo(roomId);
866
878
  } catch (err) {
867
879
  if (err instanceof StreamFetchError) {
880
+ this.emit("waiting", {
881
+ username: this.username,
882
+ phase: "stream",
883
+ elapsed: (Date.now() - waitStart) / 1e3
884
+ });
868
885
  await sleep(maxInterval);
869
886
  continue;
870
887
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokwatchr",
3
- "version": "0.4.4",
3
+ "version": "0.5.0",
4
4
  "description": "Download TikTok livestreams. Given a username, download the livestream.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.mjs",
@@ -320,6 +320,16 @@ export class TikTokLiveDownloader {
320
320
  // biome-ignore lint/style/noNonNullAssertion: at least one result
321
321
  return finalResults[finalResults.length - 1]!;
322
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
+
323
333
  // Await any background remuxes before shutting down,
324
334
  // so they can finish producing .mp4 and delete temp .ts files.
325
335
  if (pendingRemuxes.length > 0) {
@@ -368,6 +378,7 @@ export class TikTokLiveDownloader {
368
378
  private async resolveRoomIdWithRetry(): Promise<string> {
369
379
  const interval = this.options.checkInterval;
370
380
  const maxInterval = Math.max(interval, 180_000);
381
+ const waitStart = Date.now();
371
382
 
372
383
  while (true) {
373
384
  this.abortController.signal?.throwIfAborted();
@@ -377,6 +388,11 @@ export class TikTokLiveDownloader {
377
388
  return roomId;
378
389
  }
379
390
 
391
+ this.emit("waiting", {
392
+ username: this.username,
393
+ phase: "room",
394
+ elapsed: (Date.now() - waitStart) / 1_000,
395
+ });
380
396
  await sleep(maxInterval);
381
397
  }
382
398
  }
@@ -401,6 +417,7 @@ export class TikTokLiveDownloader {
401
417
  private async pollStreamInfo(roomId: string): Promise<StreamInfo> {
402
418
  const interval = this.options.checkInterval;
403
419
  const maxInterval = Math.max(interval, 180_000);
420
+ const waitStart = Date.now();
404
421
 
405
422
  while (true) {
406
423
  this.abortController.signal?.throwIfAborted();
@@ -409,6 +426,11 @@ export class TikTokLiveDownloader {
409
426
  return await this.fetchStreamInfo(roomId);
410
427
  } catch (err) {
411
428
  if (err instanceof StreamFetchError) {
429
+ this.emit("waiting", {
430
+ username: this.username,
431
+ phase: "stream",
432
+ elapsed: (Date.now() - waitStart) / 1_000,
433
+ });
412
434
  await sleep(maxInterval);
413
435
  continue;
414
436
  }
package/src/index.ts CHANGED
@@ -32,6 +32,7 @@ export type {
32
32
  StreamQualityKey,
33
33
  TikTokLiveDownloaderEvents,
34
34
  TikTokLiveDownloaderOptions,
35
+ WaitingInfo,
35
36
  } from "./types.js";
36
37
 
37
38
  // ─── Quality utilities ────────────────────────────────────
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. */