@zfadhli/tokrec 0.4.1 → 0.4.2

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.
@@ -10,9 +10,13 @@ async function createHttpClient(config) {
10
10
  os: "windows",
11
11
  proxy: config.proxy
12
12
  });
13
- if (config.cookies?.sessionid_ss) {
14
- session.setCookie("sessionid_ss", config.cookies.sessionid_ss, "https://www.tiktok.com");
15
- if (config.cookies["tt-target-idc"]) session.setCookie("tt-target-idc", config.cookies["tt-target-idc"], "https://www.tiktok.com");
13
+ if (config.cookies?.sessionid_ss) for (const domain of [
14
+ "https://www.tiktok.com",
15
+ "https://webcast.tiktok.com",
16
+ "https://m.tiktok.com"
17
+ ]) {
18
+ session.setCookie("sessionid_ss", config.cookies.sessionid_ss, domain);
19
+ if (config.cookies["tt-target-idc"]) session.setCookie("tt-target-idc", config.cookies["tt-target-idc"], domain);
16
20
  }
17
21
  return {
18
22
  get: async (url) => {
@@ -1 +1 @@
1
- {"version":3,"file":"client.mjs","names":[],"sources":["../../src/api/client.ts"],"sourcesContent":["/**\n * HTTP client — thin wrapper around wreq-js createSession.\n * Provides TLS fingerprint impersonation, cookie jar, and proxy support.\n */\n\nimport { type Session, createSession } from \"wreq-js\"\nimport type { RecorderConfig } from \"../config\"\n\nexport interface HttpClient {\n get: (url: string) => Promise<Response>\n post: (url: string, body?: BodyInit, headers?: Record<string, string>) => Promise<Response>\n close: () => Promise<void>\n}\n\nexport async function createHttpClient(config: RecorderConfig): Promise<HttpClient> {\n const session: Session = await createSession({\n browser: \"chrome_142\",\n os: \"windows\",\n proxy: config.proxy,\n } as any)\n\n // Seed cookies into the session jar so all subsequent requests are authenticated.\n // NOTE: Using a Cookie header does NOT populate wreq-js's cookie jar —\n // we must use session.setCookie() instead.\n if (config.cookies?.sessionid_ss) {\n session.setCookie(\"sessionid_ss\", config.cookies.sessionid_ss, \"https://www.tiktok.com\")\n if (config.cookies[\"tt-target-idc\"]) {\n session.setCookie(\"tt-target-idc\", config.cookies[\"tt-target-idc\"], \"https://www.tiktok.com\")\n }\n }\n\n return {\n get: async (url: string) => {\n const res = await session.fetch(url, {\n signal: AbortSignal.timeout(15000),\n } as any)\n return res as unknown as Response\n },\n post: async (url: string, body?: BodyInit, headers?: Record<string, string>) => {\n const res = await session.fetch(url, {\n method: \"POST\",\n body,\n signal: AbortSignal.timeout(15000),\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded; charset=UTF-8\",\n ...headers,\n },\n } as any)\n return res as unknown as Response\n },\n close: async () => {\n await session.close()\n },\n }\n}\n"],"mappings":";;;;;;AAcA,eAAsB,iBAAiB,QAA6C;CAClF,MAAM,UAAmB,MAAM,cAAc;EAC3C,SAAS;EACT,IAAI;EACJ,OAAO,OAAO;CAChB,CAAQ;CAKR,IAAI,OAAO,SAAS,cAAc;EAChC,QAAQ,UAAU,gBAAgB,OAAO,QAAQ,cAAc,wBAAwB;EACvF,IAAI,OAAO,QAAQ,kBACjB,QAAQ,UAAU,iBAAiB,OAAO,QAAQ,kBAAkB,wBAAwB;CAEhG;CAEA,OAAO;EACL,KAAK,OAAO,QAAgB;GAI1B,OAAO,MAHW,QAAQ,MAAM,KAAK,EACnC,QAAQ,YAAY,QAAQ,IAAK,EACnC,CAAQ;EAEV;EACA,MAAM,OAAO,KAAa,MAAiB,YAAqC;GAU9E,OAAO,MATW,QAAQ,MAAM,KAAK;IACnC,QAAQ;IACR;IACA,QAAQ,YAAY,QAAQ,IAAK;IACjC,SAAS;KACP,gBAAgB;KAChB,GAAG;IACL;GACF,CAAQ;EAEV;EACA,OAAO,YAAY;GACjB,MAAM,QAAQ,MAAM;EACtB;CACF;AACF"}
1
+ {"version":3,"file":"client.mjs","names":[],"sources":["../../src/api/client.ts"],"sourcesContent":["/**\n * HTTP client — thin wrapper around wreq-js createSession.\n * Provides TLS fingerprint impersonation, cookie jar, and proxy support.\n */\n\nimport { type Session, createSession } from \"wreq-js\"\nimport type { RecorderConfig } from \"../config\"\n\nexport interface HttpClient {\n get: (url: string) => Promise<Response>\n post: (url: string, body?: BodyInit, headers?: Record<string, string>) => Promise<Response>\n close: () => Promise<void>\n}\n\nexport async function createHttpClient(config: RecorderConfig): Promise<HttpClient> {\n const session: Session = await createSession({\n browser: \"chrome_142\",\n os: \"windows\",\n proxy: config.proxy,\n } as any)\n\n // Seed cookies into the session jar so all subsequent requests are authenticated.\n // NOTE: Using a Cookie header does NOT populate wreq-js's cookie jar —\n // we must use session.setCookie() instead.\n // Seed for all TikTok domains since cookies may not propagate across\n // subdomains automatically in wreq-js's cookie jar.\n if (config.cookies?.sessionid_ss) {\n for (const domain of [\n \"https://www.tiktok.com\",\n \"https://webcast.tiktok.com\",\n \"https://m.tiktok.com\",\n ]) {\n session.setCookie(\"sessionid_ss\", config.cookies.sessionid_ss, domain)\n if (config.cookies[\"tt-target-idc\"]) {\n session.setCookie(\"tt-target-idc\", config.cookies[\"tt-target-idc\"], domain)\n }\n }\n }\n\n return {\n get: async (url: string) => {\n const res = await session.fetch(url, {\n signal: AbortSignal.timeout(15000),\n } as any)\n return res as unknown as Response\n },\n post: async (url: string, body?: BodyInit, headers?: Record<string, string>) => {\n const res = await session.fetch(url, {\n method: \"POST\",\n body,\n signal: AbortSignal.timeout(15000),\n headers: {\n \"Content-Type\": \"application/x-www-form-urlencoded; charset=UTF-8\",\n ...headers,\n },\n } as any)\n return res as unknown as Response\n },\n close: async () => {\n await session.close()\n },\n }\n}\n"],"mappings":";;;;;;AAcA,eAAsB,iBAAiB,QAA6C;CAClF,MAAM,UAAmB,MAAM,cAAc;EAC3C,SAAS;EACT,IAAI;EACJ,OAAO,OAAO;CAChB,CAAQ;CAOR,IAAI,OAAO,SAAS,cAClB,KAAK,MAAM,UAAU;EACnB;EACA;EACA;CACF,GAAG;EACD,QAAQ,UAAU,gBAAgB,OAAO,QAAQ,cAAc,MAAM;EACrE,IAAI,OAAO,QAAQ,kBACjB,QAAQ,UAAU,iBAAiB,OAAO,QAAQ,kBAAkB,MAAM;CAE9E;CAGF,OAAO;EACL,KAAK,OAAO,QAAgB;GAI1B,OAAO,MAHW,QAAQ,MAAM,KAAK,EACnC,QAAQ,YAAY,QAAQ,IAAK,EACnC,CAAQ;EAEV;EACA,MAAM,OAAO,KAAa,MAAiB,YAAqC;GAU9E,OAAO,MATW,QAAQ,MAAM,KAAK;IACnC,QAAQ;IACR;IACA,QAAQ,YAAY,QAAQ,IAAK;IACjC,SAAS;KACP,gBAAgB;KAChB,GAAG;IACL;GACF,CAAQ;EAEV;EACA,OAAO,YAAY;GACjB,MAAM,QAAQ,MAAM;EACtB;CACF;AACF"}
@@ -29,33 +29,92 @@ function createTikTokApi(http) {
29
29
  };
30
30
  async function fetchLiveInfo(user) {
31
31
  let sigi = null;
32
+ let html = "";
32
33
  try {
33
34
  const res = await http.get(`${TIKTOK_BASE}/@${user}/live`);
34
35
  if (!res.ok) return null;
35
- sigi = extractSigiState(await res.text());
36
- if (!sigi) return null;
37
- const liveRoom = sigi.LiveRoom?.liveRoomUserInfo?.liveRoom;
38
- const userInfo = sigi.LiveRoom?.liveRoomUserInfo?.user;
39
- if (liveRoom && userInfo?.roomId) {
40
- const roomId = String(userInfo.roomId);
41
- const isLive = liveRoom.status === 2;
42
- let streamUrl = null;
43
- if (isLive) {
44
- streamUrl = extractStreamUrlFast(liveRoom);
45
- if (!streamUrl) streamUrl = findStreamUrlRecursively(sigi);
46
- if (!streamUrl) streamUrl = await fetchStreamUrlFromApi(roomId);
36
+ html = await res.text();
37
+ sigi = extractSigiState(html);
38
+ if (sigi) {
39
+ const liveRoom = sigi.LiveRoom?.liveRoomUserInfo?.liveRoom;
40
+ const userInfo = sigi.LiveRoom?.liveRoomUserInfo?.user;
41
+ if (liveRoom && userInfo?.roomId) {
42
+ const roomId = String(userInfo.roomId);
43
+ const isLive = liveRoom.status === 2;
44
+ let streamUrl = null;
45
+ if (isLive) {
46
+ streamUrl = extractStreamUrlFast(liveRoom);
47
+ if (!streamUrl) streamUrl = findStreamUrlRecursively(sigi);
48
+ if (!streamUrl) streamUrl = await fetchStreamUrlFromApi(roomId);
49
+ }
50
+ return {
51
+ roomId,
52
+ isLive,
53
+ streamUrl,
54
+ title: liveRoom.title ?? null
55
+ };
47
56
  }
48
- return {
49
- roomId,
50
- isLive,
51
- streamUrl,
52
- title: liveRoom.title ?? null
53
- };
57
+ const legacy = await fetchLiveInfoFallback(sigi, user);
58
+ if (legacy) return legacy;
54
59
  }
55
60
  } catch (_err) {
56
61
  return null;
57
62
  }
58
- return fetchLiveInfoFallback(sigi, user);
63
+ const { userId, roomId } = extractUserFromUniversalData(html);
64
+ if (roomId) {
65
+ const result = await fetchRoomInfoFromRoomApi(roomId);
66
+ if (result) return result;
67
+ }
68
+ if (userId) {
69
+ const result = await fetchRoomInfoFromApi(userId);
70
+ if (result) return result;
71
+ }
72
+ return fetchLiveInfoFromProfile(user);
73
+ }
74
+ /**
75
+ * Fetch the main profile page to extract user info from
76
+ * __UNIVERSAL_DATA_FOR_REHYDRATION__. When the user is live, their
77
+ * roomId is embedded in the data, allowing us to fetch room info and
78
+ * stream URL via the /webcast/room/info/ endpoint (which works
79
+ * reliably). Falls back to room/enter/ via the numeric userId.
80
+ */
81
+ async function fetchLiveInfoFromProfile(user) {
82
+ try {
83
+ const res = await http.get(`${TIKTOK_BASE}/@${user}`);
84
+ if (!res.ok) return null;
85
+ const { userId, roomId } = extractUserFromUniversalData(await res.text());
86
+ if (!userId) return null;
87
+ if (roomId) return fetchRoomInfoFromRoomApi(roomId);
88
+ return fetchRoomInfoFromApi(userId);
89
+ } catch {
90
+ return null;
91
+ }
92
+ }
93
+ /**
94
+ * Fetch room info from the Webcast room/info API (reliable, works
95
+ * for both live and offline rooms). Requires a known roomId.
96
+ */
97
+ async function fetchRoomInfoFromRoomApi(roomId) {
98
+ try {
99
+ const url = `${WEBCAST_BASE}/webcast/room/info/?aid=1988&room_id=${roomId}&type=live`;
100
+ const res = await http.get(url);
101
+ if (!res.ok) return null;
102
+ const text = await res.text();
103
+ if (text.length === 0) return null;
104
+ const data = JSON.parse(text);
105
+ if (data.status_code === 4003110) return null;
106
+ const roomData = data.data;
107
+ if (!roomData) return null;
108
+ const isLive = (roomData.status ?? 4) === 2;
109
+ return {
110
+ roomId,
111
+ isLive,
112
+ streamUrl: isLive ? findStreamUrlRecursively(roomData) : null,
113
+ title: roomData.title ?? null
114
+ };
115
+ } catch {
116
+ return null;
117
+ }
59
118
  }
60
119
  /**
61
120
  * Secondary live-info extraction when SIGI_STATE's LiveRoom data is
@@ -137,6 +196,37 @@ function extractSigiState(html) {
137
196
  }
138
197
  }
139
198
  /**
199
+ * Extract the numeric user ID and optional room ID from TikTok's new
200
+ * page structure (__UNIVERSAL_DATA_FOR_REHYDRATION__).
201
+ *
202
+ * The profile page (/@user) includes "webapp.user-detail" which
203
+ * contains the user's numeric ID. When the user is live, the roomId
204
+ * is also embedded here.
205
+ *
206
+ * Returns { userId, roomId } where userId is always set when the data
207
+ * is found, and roomId is set only when the user is currently live.
208
+ */
209
+ function extractUserFromUniversalData(html) {
210
+ const match = html.match(/<script\s+id="__UNIVERSAL_DATA_FOR_REHYDRATION__"\s+type="application\/json">(.*?)<\/script>/);
211
+ if (!match?.[1]) return {
212
+ userId: null,
213
+ roomId: null
214
+ };
215
+ try {
216
+ const data = JSON.parse(match[1]);
217
+ const user = ((data.__DEFAULT_SCOPE__ ?? data)?.["webapp.user-detail"])?.userInfo?.user;
218
+ return {
219
+ userId: user?.id ? String(user.id) : null,
220
+ roomId: user?.roomId ? String(user.roomId) : null
221
+ };
222
+ } catch {
223
+ return {
224
+ userId: null,
225
+ roomId: null
226
+ };
227
+ }
228
+ }
229
+ /**
140
230
  * Fast-path: extract stream URL from the known SIGI_STATE location.
141
231
  * This covers ~95% of cases without needing a full recursive search.
142
232
  */
@@ -1 +1 @@
1
- {"version":3,"file":"tiktok.mjs","names":[],"sources":["../../src/api/tiktok.ts"],"sourcesContent":["/**\n * TikTok API — resolves room IDs, live status, and stream URLs.\n *\n * Primary path: parse SIGI_STATE from the user's /live page (fast, ~95% of\n * cases). Fallback paths handle async-loaded room info by reading\n * UserModule / LiveRoom.liveRoomStatus from SIGI_STATE and, when needed,\n * calling the Webcast API (room/info/ or room/enter/).\n *\n * Architecture:\n * createTikTokApi(httpClient) → { getRoomId, isRoomAlive, getLiveUrl }\n *\n * All public methods share a per-tick cache so the page is fetched at most once\n * per poll cycle.\n */\n\nimport type { HttpClient } from \"./client\"\n\nexport interface TikTokApi {\n /** Resolve a username to a room ID. Returns null if not found. */\n getRoomId(user: string): Promise<string | null>\n /** Check if a room is currently live. */\n isRoomAlive(roomId: string): Promise<boolean>\n /** Get the best-quality FLV stream URL for a room. Returns null on failure. */\n getLiveUrl(roomId: string): Promise<string | null>\n /** Invalidate per-tick cache (call at start of each poll tick). */\n invalidateCache(): void\n}\n\nconst TIKTOK_BASE = \"https://www.tiktok.com\"\nconst WEBCAST_BASE = \"https://webcast.tiktok.com\"\n\nexport function createTikTokApi(http: HttpClient): TikTokApi {\n // Per-tick cache — stores the last fetched live info so the three public\n // methods don't each fetch the page separately.\n let cached: LiveInfo | null = null\n let cachedUser = \"\"\n\n async function ensureCache(user: string): Promise<LiveInfo | null> {\n if (cached && cachedUser === user) return cached\n cached = await fetchLiveInfo(user)\n cachedUser = user\n return cached\n }\n\n function invalidateCache(): void {\n cached = null\n cachedUser = \"\"\n }\n\n return {\n invalidateCache,\n\n async getRoomId(user: string): Promise<string | null> {\n const info = await ensureCache(user)\n return info?.roomId ?? null\n },\n\n async isRoomAlive(roomId: string): Promise<boolean> {\n if (cached && String(cached.roomId) === String(roomId)) {\n return cached.isLive\n }\n const info = await fetchLiveInfo(cachedUser || roomId)\n return info?.isLive ?? false\n },\n\n async getLiveUrl(roomId: string): Promise<string | null> {\n const info = await ensureCache(cachedUser || roomId)\n return info?.streamUrl ?? null\n },\n }\n\n // ─── Internal: fetch and parse SIGI_STATE ─────────────────\n\n async function fetchLiveInfo(user: string): Promise<LiveInfo | null> {\n // Track whether SIGI_STATE actually existed (for retry decisions)\n let sigi: SigiState | null = null\n\n try {\n const res = await http.get(`${TIKTOK_BASE}/@${user}/live`)\n if (!res.ok) return null\n\n const html = await res.text()\n sigi = extractSigiState(html)\n if (!sigi) return null\n\n // ── Primary path: room info in LiveRoom.liveRoomUserInfo ──\n const liveRoom = sigi.LiveRoom?.liveRoomUserInfo?.liveRoom\n const userInfo = sigi.LiveRoom?.liveRoomUserInfo?.user\n\n if (liveRoom && userInfo?.roomId) {\n const roomId = String(userInfo.roomId)\n const isLive = liveRoom.status === 2 // 2 = live, 4 = offline\n\n // Extract stream URL: try SIGI_STATE first, then Webcast API fallback\n let streamUrl: string | null = null\n if (isLive) {\n streamUrl = extractStreamUrlFast(liveRoom)\n if (!streamUrl) streamUrl = findStreamUrlRecursively(sigi)\n if (!streamUrl) streamUrl = await fetchStreamUrlFromApi(roomId)\n }\n\n return { roomId, isLive, streamUrl, title: liveRoom.title ?? null }\n }\n } catch (_err) {\n // Timeout (15s) or network error → treat as offline\n return null\n }\n\n // ── Fallback path: async-loaded room info ──\n // When LiveRoom.liveRoomUserInfo is absent, check LiveRoom.liveRoomStatus\n // and UserModule.users[user].roomId, then use the Webcast API for stream URL.\n return fetchLiveInfoFallback(sigi, user)\n }\n\n /**\n * Secondary live-info extraction when SIGI_STATE's LiveRoom data is\n * async-loaded. Uses LiveRoom.liveRoomStatus for the live flag and\n * UserModule.users[user].roomId for the room ID. Stream URL is obtained\n * from the Webcast API (room/info/) as a final fallback.\n */\n async function fetchLiveInfoFallback(sigi: SigiState, user: string): Promise<LiveInfo | null> {\n const liveRoomStatus = sigi.LiveRoom?.liveRoomStatus\n const isLive = liveRoomStatus === 2 // 2 = live\n\n // Try to get roomId from UserModule (usually populated even when LiveRoom is not)\n const userModule = sigi.UserModule?.users?.[user]\n let roomId: string | null = null\n\n if (userModule?.roomId) {\n roomId =\n typeof userModule.roomId === \"object\"\n ? String((userModule.roomId as { id: string }).id ?? \"\")\n : String(userModule.roomId)\n }\n\n // If we have a roomId, use Webcast room/info for the stream URL\n if (roomId) {\n const streamUrl = isLive ? await fetchStreamUrlFromApi(roomId) : null\n return { roomId, isLive, streamUrl, title: null }\n }\n\n // Final fallback: Webcast room/enter API using the numeric user ID\n if (userModule?.id && isLive) {\n return fetchRoomInfoFromApi(userModule.id)\n }\n\n return null // truly offline / unknown\n }\n\n /**\n * Fallback: fetch room info + stream URL from the Webcast API.\n * This covers cases where SIGI_STATE has no room data at all\n * (fully async-loaded page).\n */\n async function fetchRoomInfoFromApi(userId: string): Promise<LiveInfo | null> {\n try {\n const url = `${WEBCAST_BASE}/webcast/room/enter/?aid=1988&user_id=${userId}`\n const res = await http.get(url)\n if (!res.ok) return null\n\n const text = await res.text()\n if (text.length === 0) return null\n\n const data = JSON.parse(text) as Record<string, unknown>\n const roomData = (data as any)?.data?.room\n if (!roomData) return null\n\n const roomId = String(roomData.roomId ?? \"\")\n if (!roomId) return null\n\n const roomStatus = roomData.status // 2 = live\n const isLive = roomStatus === 2\n const streamUrl = isLive ? findStreamUrlRecursively(roomData) : null\n\n return { roomId, isLive, streamUrl, title: roomData.title ?? null }\n } catch {\n return null\n }\n }\n\n /**\n * Fallback: fetch stream URL from the Webcast API.\n * This covers cases where SIGI_STATE doesn't include stream data\n * (e.g. async-loaded room info).\n */\n async function fetchStreamUrlFromApi(roomId: string): Promise<string | null> {\n try {\n const url = `${WEBCAST_BASE}/webcast/room/info/?aid=1988&room_id=${roomId}&type=live`\n const res = await http.get(url)\n if (!res.ok) return null\n\n const text = await res.text()\n if (text.length === 0) return null\n\n const data = JSON.parse(text) as Record<string, unknown>\n\n // status_code 4003110 = live restriction / room not found\n if ((data as any).status_code === 4003110) return null\n\n return findStreamUrlRecursively(data)\n } catch {\n return null\n }\n }\n}\n\n// ─── Types ──────────────────────────────────────────────────\n\ninterface LiveInfo {\n roomId: string\n isLive: boolean\n streamUrl: string | null\n title: string | null\n}\n\ninterface SigiState {\n LiveRoom?: {\n liveRoomStatus?: number\n liveRoomUserInfo?: {\n user?: {\n id?: string | number\n roomId?: string | number\n }\n liveRoom?: {\n status?: number // 2 = live, 4 = offline\n title?: string\n startTime?: number\n streamData?: {\n pull_data?: {\n stream_data?: string // JSON string with FLV/HLS URLs\n }\n }\n }\n }\n }\n UserModule?: {\n users?: {\n [username: string]: {\n id?: string // numeric user ID\n uniqueId?: string\n roomId?: string | { id: string }\n }\n }\n }\n}\n\n// ─── HTML parsing ──────────────────────────────────────────\n\nfunction extractSigiState(html: string): SigiState | null {\n const match = html.match(/<script\\s+id=\"SIGI_STATE\"\\s+type=\"application\\/json\">(.*?)<\\/script>/)\n if (!match?.[1]) return null\n try {\n return JSON.parse(match[1]) as SigiState\n } catch {\n return null\n }\n}\n\n// ─── Stream URL extraction ─────────────────────────────────\n\n/**\n * Fast-path: extract stream URL from the known SIGI_STATE location.\n * This covers ~95% of cases without needing a full recursive search.\n */\nfunction extractStreamUrlFast(\n liveRoom: NonNullable<NonNullable<SigiState[\"LiveRoom\"]>[\"liveRoomUserInfo\"]>[\"liveRoom\"],\n): string | null {\n try {\n const raw = liveRoom?.streamData?.pull_data?.stream_data\n if (!raw) return null\n const parsed = JSON.parse(raw) as Record<string, unknown>\n const data = (parsed as Record<string, unknown>)?.data as Record<string, unknown> | undefined\n if (!data) return null\n\n // Try all known quality keys, preferring higher quality\n const qualities = [\"origin\", \"fhd\", \"uhd\", \"hd\", \"sd\", \"ld\"]\n for (const q of qualities) {\n const entry = data[q] as { main?: { flv?: string } } | undefined\n const url = entry?.main?.flv\n if (url) return url\n }\n return null\n } catch {\n return null\n }\n}\n\n/**\n * Brute-force recursive search for a stream URL anywhere in a JSON value.\n * Survives TikTok changing key names or nesting structure.\n *\n * Inspired by PR #430 (Michele0303/tiktok-live-recorder).\n */\nexport function findStreamUrlRecursively(obj: unknown): string | null {\n // Base case: a string\n if (typeof obj === \"string\") {\n // Direct URL match\n if (\n (obj.startsWith(\"http://\") || obj.startsWith(\"https://\")) &&\n (obj.includes(\".flv\") || obj.includes(\".m3u8\"))\n ) {\n return obj\n }\n // JSON-encoded string — parse and recurse (covers stream_data, etc.)\n if (obj.startsWith(\"{\") || obj.startsWith(\"[\")) {\n try {\n return findStreamUrlRecursively(JSON.parse(obj))\n } catch {\n return null\n }\n }\n return null\n }\n\n // Dict: recurse into all values\n if (obj && typeof obj === \"object\" && !Array.isArray(obj)) {\n for (const val of Object.values(obj as Record<string, unknown>)) {\n const found = findStreamUrlRecursively(val)\n if (found) return found\n }\n return null\n }\n\n // Array: recurse into all elements\n if (Array.isArray(obj)) {\n for (const item of obj) {\n const found = findStreamUrlRecursively(item)\n if (found) return found\n }\n }\n\n return null\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"tiktok.mjs","names":[],"sources":["../../src/api/tiktok.ts"],"sourcesContent":["/**\n * TikTok API — resolves room IDs, live status, and stream URLs.\n *\n * Primary path: parse SIGI_STATE from the user's /live page (fast path for\n * older TikTok pages). When SIGI_STATE is absent (TikTok's new unified page\n * structure), falls back to __UNIVERSAL_DATA_FOR_REHYDRATION__ on the profile\n * page to extract the numeric user ID, then calls the Webcast API\n * (room/enter/) for live status and stream URL.\n *\n * Architecture:\n * createTikTokApi(httpClient) → { getRoomId, isRoomAlive, getLiveUrl }\n *\n * All public methods share a per-tick cache so the page is fetched at most once\n * per poll cycle.\n */\n\nimport type { HttpClient } from \"./client\"\n\nexport interface TikTokApi {\n /** Resolve a username to a room ID. Returns null if not found. */\n getRoomId(user: string): Promise<string | null>\n /** Check if a room is currently live. */\n isRoomAlive(roomId: string): Promise<boolean>\n /** Get the best-quality FLV stream URL for a room. Returns null on failure. */\n getLiveUrl(roomId: string): Promise<string | null>\n /** Invalidate per-tick cache (call at start of each poll tick). */\n invalidateCache(): void\n}\n\nconst TIKTOK_BASE = \"https://www.tiktok.com\"\nconst WEBCAST_BASE = \"https://webcast.tiktok.com\"\n\nexport function createTikTokApi(http: HttpClient): TikTokApi {\n // Per-tick cache — stores the last fetched live info so the three public\n // methods don't each fetch the page separately.\n let cached: LiveInfo | null = null\n let cachedUser = \"\"\n\n async function ensureCache(user: string): Promise<LiveInfo | null> {\n if (cached && cachedUser === user) return cached\n cached = await fetchLiveInfo(user)\n cachedUser = user\n return cached\n }\n\n function invalidateCache(): void {\n cached = null\n cachedUser = \"\"\n }\n\n return {\n invalidateCache,\n\n async getRoomId(user: string): Promise<string | null> {\n const info = await ensureCache(user)\n return info?.roomId ?? null\n },\n\n async isRoomAlive(roomId: string): Promise<boolean> {\n if (cached && String(cached.roomId) === String(roomId)) {\n return cached.isLive\n }\n const info = await fetchLiveInfo(cachedUser || roomId)\n return info?.isLive ?? false\n },\n\n async getLiveUrl(roomId: string): Promise<string | null> {\n const info = await ensureCache(cachedUser || roomId)\n return info?.streamUrl ?? null\n },\n }\n\n // ─── Internal: fetch and parse SIGI_STATE / Universal Data ──\n\n async function fetchLiveInfo(user: string): Promise<LiveInfo | null> {\n let sigi: SigiState | null = null\n let html = \"\"\n\n try {\n const res = await http.get(`${TIKTOK_BASE}/@${user}/live`)\n if (!res.ok) return null\n\n html = await res.text()\n sigi = extractSigiState(html)\n\n if (sigi) {\n // ── Primary path: room info in LiveRoom.liveRoomUserInfo ──\n const liveRoom = sigi.LiveRoom?.liveRoomUserInfo?.liveRoom\n const userInfo = sigi.LiveRoom?.liveRoomUserInfo?.user\n\n if (liveRoom && userInfo?.roomId) {\n const roomId = String(userInfo.roomId)\n const isLive = liveRoom.status === 2 // 2 = live, 4 = offline\n\n // Extract stream URL: try SIGI_STATE first, then Webcast API fallback\n let streamUrl: string | null = null\n if (isLive) {\n streamUrl = extractStreamUrlFast(liveRoom)\n if (!streamUrl) streamUrl = findStreamUrlRecursively(sigi)\n if (!streamUrl) streamUrl = await fetchStreamUrlFromApi(roomId)\n }\n\n return { roomId, isLive, streamUrl, title: liveRoom.title ?? null }\n }\n\n // ── Legacy fallback: async-loaded SIGI_STATE ──\n const legacy = await fetchLiveInfoFallback(sigi, user)\n if (legacy) return legacy\n }\n } catch (_err) {\n // Timeout (15s) or network error → treat as offline\n return null\n }\n\n // ── New fallback: TikTok's unified page structure ──\n // TikTok migrated from SIGI_STATE to __UNIVERSAL_DATA_FOR_REHYDRATION__.\n // The /live page may not have it, so try the main profile page.\n const { userId, roomId } = extractUserFromUniversalData(html)\n if (roomId) {\n // If the /live page has the roomId, use room/info (reliable)\n const result = await fetchRoomInfoFromRoomApi(roomId)\n if (result) return result\n }\n if (userId) {\n // Fallback: try room/enter with the numeric user ID\n const result = await fetchRoomInfoFromApi(userId)\n if (result) return result\n }\n\n // Final fallback: fetch the main profile page for webapp.user-detail\n return fetchLiveInfoFromProfile(user)\n }\n\n /**\n * Fetch the main profile page to extract user info from\n * __UNIVERSAL_DATA_FOR_REHYDRATION__. When the user is live, their\n * roomId is embedded in the data, allowing us to fetch room info and\n * stream URL via the /webcast/room/info/ endpoint (which works\n * reliably). Falls back to room/enter/ via the numeric userId.\n */\n async function fetchLiveInfoFromProfile(user: string): Promise<LiveInfo | null> {\n try {\n const res = await http.get(`${TIKTOK_BASE}/@${user}`)\n if (!res.ok) return null\n\n const html = await res.text()\n const { userId, roomId } = extractUserFromUniversalData(html)\n if (!userId) return null\n\n // If roomId is embedded in the profile page, the user is live.\n // Use room/info which works reliably for both live and offline rooms.\n if (roomId) {\n return fetchRoomInfoFromRoomApi(roomId)\n }\n\n // Fallback: try Webcast room/enter with the numeric userId\n return fetchRoomInfoFromApi(userId)\n } catch {\n return null\n }\n }\n\n /**\n * Fetch room info from the Webcast room/info API (reliable, works\n * for both live and offline rooms). Requires a known roomId.\n */\n async function fetchRoomInfoFromRoomApi(roomId: string): Promise<LiveInfo | null> {\n try {\n const url = `${WEBCAST_BASE}/webcast/room/info/?aid=1988&room_id=${roomId}&type=live`\n const res = await http.get(url)\n if (!res.ok) return null\n\n const text = await res.text()\n if (text.length === 0) return null\n\n const data = JSON.parse(text) as Record<string, unknown>\n\n // status_code 4003110 = live restriction / room not found\n if ((data as any).status_code === 4003110) return null\n\n const roomData = (data as any).data\n if (!roomData) return null\n\n const status = roomData.status ?? 4\n const isLive = status === 2\n const streamUrl = isLive ? findStreamUrlRecursively(roomData) : null\n\n return { roomId, isLive, streamUrl, title: roomData.title ?? null }\n } catch {\n return null\n }\n }\n\n /**\n * Secondary live-info extraction when SIGI_STATE's LiveRoom data is\n * async-loaded. Uses LiveRoom.liveRoomStatus for the live flag and\n * UserModule.users[user].roomId for the room ID. Stream URL is obtained\n * from the Webcast API (room/info/) as a final fallback.\n */\n async function fetchLiveInfoFallback(sigi: SigiState, user: string): Promise<LiveInfo | null> {\n const liveRoomStatus = sigi.LiveRoom?.liveRoomStatus\n const isLive = liveRoomStatus === 2 // 2 = live\n\n // Try to get roomId from UserModule (usually populated even when LiveRoom is not)\n const userModule = sigi.UserModule?.users?.[user]\n let roomId: string | null = null\n\n if (userModule?.roomId) {\n roomId =\n typeof userModule.roomId === \"object\"\n ? String((userModule.roomId as { id: string }).id ?? \"\")\n : String(userModule.roomId)\n }\n\n // If we have a roomId, use Webcast room/info for the stream URL\n if (roomId) {\n const streamUrl = isLive ? await fetchStreamUrlFromApi(roomId) : null\n return { roomId, isLive, streamUrl, title: null }\n }\n\n // Final fallback: Webcast room/enter API using the numeric user ID\n if (userModule?.id && isLive) {\n return fetchRoomInfoFromApi(userModule.id)\n }\n\n return null // truly offline / unknown\n }\n\n /**\n * Fallback: fetch room info + stream URL from the Webcast API.\n * This covers cases where SIGI_STATE has no room data at all\n * (fully async-loaded page).\n */\n async function fetchRoomInfoFromApi(userId: string): Promise<LiveInfo | null> {\n try {\n const url = `${WEBCAST_BASE}/webcast/room/enter/?aid=1988&user_id=${userId}`\n const res = await http.get(url)\n if (!res.ok) return null\n\n const text = await res.text()\n if (text.length === 0) return null\n\n const data = JSON.parse(text) as Record<string, unknown>\n const roomData = (data as any)?.data?.room\n if (!roomData) return null\n\n const roomId = String(roomData.roomId ?? \"\")\n if (!roomId) return null\n\n const roomStatus = roomData.status // 2 = live\n const isLive = roomStatus === 2\n const streamUrl = isLive ? findStreamUrlRecursively(roomData) : null\n\n return { roomId, isLive, streamUrl, title: roomData.title ?? null }\n } catch {\n return null\n }\n }\n\n /**\n * Fallback: fetch stream URL from the Webcast API.\n * This covers cases where SIGI_STATE doesn't include stream data\n * (e.g. async-loaded room info).\n */\n async function fetchStreamUrlFromApi(roomId: string): Promise<string | null> {\n try {\n const url = `${WEBCAST_BASE}/webcast/room/info/?aid=1988&room_id=${roomId}&type=live`\n const res = await http.get(url)\n if (!res.ok) return null\n\n const text = await res.text()\n if (text.length === 0) return null\n\n const data = JSON.parse(text) as Record<string, unknown>\n\n // status_code 4003110 = live restriction / room not found\n if ((data as any).status_code === 4003110) return null\n\n return findStreamUrlRecursively(data)\n } catch {\n return null\n }\n }\n}\n\n// ─── Types ──────────────────────────────────────────────────\n\ninterface LiveInfo {\n roomId: string\n isLive: boolean\n streamUrl: string | null\n title: string | null\n}\n\ninterface SigiState {\n LiveRoom?: {\n liveRoomStatus?: number\n liveRoomUserInfo?: {\n user?: {\n id?: string | number\n roomId?: string | number\n }\n liveRoom?: {\n status?: number // 2 = live, 4 = offline\n title?: string\n startTime?: number\n streamData?: {\n pull_data?: {\n stream_data?: string // JSON string with FLV/HLS URLs\n }\n }\n }\n }\n }\n UserModule?: {\n users?: {\n [username: string]: {\n id?: string // numeric user ID\n uniqueId?: string\n roomId?: string | { id: string }\n }\n }\n }\n}\n\n// ─── HTML parsing ──────────────────────────────────────────\n\nfunction extractSigiState(html: string): SigiState | null {\n const match = html.match(/<script\\s+id=\"SIGI_STATE\"\\s+type=\"application\\/json\">(.*?)<\\/script>/)\n if (!match?.[1]) return null\n try {\n return JSON.parse(match[1]) as SigiState\n } catch {\n return null\n }\n}\n\n/**\n * Extract the numeric user ID and optional room ID from TikTok's new\n * page structure (__UNIVERSAL_DATA_FOR_REHYDRATION__).\n *\n * The profile page (/@user) includes \"webapp.user-detail\" which\n * contains the user's numeric ID. When the user is live, the roomId\n * is also embedded here.\n *\n * Returns { userId, roomId } where userId is always set when the data\n * is found, and roomId is set only when the user is currently live.\n */\nfunction extractUserFromUniversalData(html: string): {\n userId: string | null\n roomId: string | null\n} {\n const match = html.match(\n /<script\\s+id=\"__UNIVERSAL_DATA_FOR_REHYDRATION__\"\\s+type=\"application\\/json\">(.*?)<\\/script>/,\n )\n if (!match?.[1]) return { userId: null, roomId: null }\n try {\n const data = JSON.parse(match[1])\n const scope = (data as any).__DEFAULT_SCOPE__ ?? data\n const userDetail = scope?.[\"webapp.user-detail\"]\n const user = userDetail?.userInfo?.user\n const userId = user?.id ? String(user.id) : null\n const roomId = user?.roomId ? String(user.roomId) : null\n return { userId, roomId }\n } catch {\n return { userId: null, roomId: null }\n }\n}\n\n// ─── Stream URL extraction ─────────────────────────────────\n\n/**\n * Fast-path: extract stream URL from the known SIGI_STATE location.\n * This covers ~95% of cases without needing a full recursive search.\n */\nfunction extractStreamUrlFast(\n liveRoom: NonNullable<NonNullable<SigiState[\"LiveRoom\"]>[\"liveRoomUserInfo\"]>[\"liveRoom\"],\n): string | null {\n try {\n const raw = liveRoom?.streamData?.pull_data?.stream_data\n if (!raw) return null\n const parsed = JSON.parse(raw) as Record<string, unknown>\n const data = (parsed as Record<string, unknown>)?.data as Record<string, unknown> | undefined\n if (!data) return null\n\n // Try all known quality keys, preferring higher quality\n const qualities = [\"origin\", \"fhd\", \"uhd\", \"hd\", \"sd\", \"ld\"]\n for (const q of qualities) {\n const entry = data[q] as { main?: { flv?: string } } | undefined\n const url = entry?.main?.flv\n if (url) return url\n }\n return null\n } catch {\n return null\n }\n}\n\n/**\n * Brute-force recursive search for a stream URL anywhere in a JSON value.\n * Survives TikTok changing key names or nesting structure.\n *\n * Inspired by PR #430 (Michele0303/tiktok-live-recorder).\n */\nexport function findStreamUrlRecursively(obj: unknown): string | null {\n // Base case: a string\n if (typeof obj === \"string\") {\n // Direct URL match\n if (\n (obj.startsWith(\"http://\") || obj.startsWith(\"https://\")) &&\n (obj.includes(\".flv\") || obj.includes(\".m3u8\"))\n ) {\n return obj\n }\n // JSON-encoded string — parse and recurse (covers stream_data, etc.)\n if (obj.startsWith(\"{\") || obj.startsWith(\"[\")) {\n try {\n return findStreamUrlRecursively(JSON.parse(obj))\n } catch {\n return null\n }\n }\n return null\n }\n\n // Dict: recurse into all values\n if (obj && typeof obj === \"object\" && !Array.isArray(obj)) {\n for (const val of Object.values(obj as Record<string, unknown>)) {\n const found = findStreamUrlRecursively(val)\n if (found) return found\n }\n return null\n }\n\n // Array: recurse into all elements\n if (Array.isArray(obj)) {\n for (const item of obj) {\n const found = findStreamUrlRecursively(item)\n if (found) return found\n }\n }\n\n return null\n}\n"],"mappings":"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"}
package/dist/package.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  //#region package.json
2
- var version = "0.4.1";
2
+ var version = "0.4.2";
3
3
  //#endregion
4
4
  export { version };
5
5
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zfadhli/tokrec",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Minimal TikTok live stream recorder — Bun + TypeScript",
5
5
  "type": "module",
6
6
  "main": "./dist/lib.mjs",