@zfadhli/tokrec 0.3.0 → 0.3.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/tiktok.mjs +64 -11
- package/dist/api/tiktok.mjs.map +1 -1
- package/dist/package.mjs +1 -1
- package/package.json +1 -1
package/dist/api/tiktok.mjs
CHANGED
|
@@ -28,29 +28,82 @@ function createTikTokApi(http) {
|
|
|
28
28
|
}
|
|
29
29
|
};
|
|
30
30
|
async function fetchLiveInfo(user) {
|
|
31
|
+
let sigi = null;
|
|
31
32
|
try {
|
|
32
33
|
const res = await http.get(`${TIKTOK_BASE}/@${user}/live`);
|
|
33
34
|
if (!res.ok) return null;
|
|
34
|
-
|
|
35
|
+
sigi = extractSigiState(await res.text());
|
|
35
36
|
if (!sigi) return null;
|
|
36
37
|
const liveRoom = sigi.LiveRoom?.liveRoomUserInfo?.liveRoom;
|
|
37
38
|
const userInfo = sigi.LiveRoom?.liveRoomUserInfo?.user;
|
|
38
|
-
if (
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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);
|
|
47
|
+
}
|
|
48
|
+
return {
|
|
49
|
+
roomId,
|
|
50
|
+
isLive,
|
|
51
|
+
streamUrl,
|
|
52
|
+
title: liveRoom.title ?? null
|
|
53
|
+
};
|
|
46
54
|
}
|
|
55
|
+
} catch (_err) {
|
|
56
|
+
return null;
|
|
57
|
+
}
|
|
58
|
+
return fetchLiveInfoFallback(sigi, user);
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Secondary live-info extraction when SIGI_STATE's LiveRoom data is
|
|
62
|
+
* async-loaded. Uses LiveRoom.liveRoomStatus for the live flag and
|
|
63
|
+
* UserModule.users[user].roomId for the room ID. Stream URL is obtained
|
|
64
|
+
* from the Webcast API (room/info/) as a final fallback.
|
|
65
|
+
*/
|
|
66
|
+
async function fetchLiveInfoFallback(sigi, user) {
|
|
67
|
+
const isLive = sigi.LiveRoom?.liveRoomStatus === 2;
|
|
68
|
+
const userModule = sigi.UserModule?.users?.[user];
|
|
69
|
+
let roomId = null;
|
|
70
|
+
if (userModule?.roomId) roomId = typeof userModule.roomId === "object" ? String(userModule.roomId.id ?? "") : String(userModule.roomId);
|
|
71
|
+
if (roomId) {
|
|
72
|
+
const streamUrl = isLive ? await fetchStreamUrlFromApi(roomId) : null;
|
|
47
73
|
return {
|
|
48
74
|
roomId,
|
|
49
75
|
isLive,
|
|
50
76
|
streamUrl,
|
|
51
|
-
title:
|
|
77
|
+
title: null
|
|
52
78
|
};
|
|
53
|
-
}
|
|
79
|
+
}
|
|
80
|
+
if (userModule?.id && isLive) return fetchRoomInfoFromApi(userModule.id);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Fallback: fetch room info + stream URL from the Webcast API.
|
|
85
|
+
* This covers cases where SIGI_STATE has no room data at all
|
|
86
|
+
* (fully async-loaded page).
|
|
87
|
+
*/
|
|
88
|
+
async function fetchRoomInfoFromApi(userId) {
|
|
89
|
+
try {
|
|
90
|
+
const url = `${WEBCAST_BASE}/webcast/room/enter/?aid=1988&user_id=${userId}`;
|
|
91
|
+
const res = await http.get(url);
|
|
92
|
+
if (!res.ok) return null;
|
|
93
|
+
const text = await res.text();
|
|
94
|
+
if (text.length === 0) return null;
|
|
95
|
+
const roomData = JSON.parse(text)?.data?.room;
|
|
96
|
+
if (!roomData) return null;
|
|
97
|
+
const roomId = String(roomData.roomId ?? "");
|
|
98
|
+
if (!roomId) return null;
|
|
99
|
+
const isLive = roomData.status === 2;
|
|
100
|
+
return {
|
|
101
|
+
roomId,
|
|
102
|
+
isLive,
|
|
103
|
+
streamUrl: isLive ? findStreamUrlRecursively(roomData) : null,
|
|
104
|
+
title: roomData.title ?? null
|
|
105
|
+
};
|
|
106
|
+
} catch {
|
|
54
107
|
return null;
|
|
55
108
|
}
|
|
56
109
|
}
|
package/dist/api/tiktok.mjs.map
CHANGED
|
@@ -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 from a single\n * SIGI_STATE HTML scrape. No webcast API calls needed.\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 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 const sigi = extractSigiState(html)\n if (!sigi) return null\n\n const liveRoom = sigi.LiveRoom?.liveRoomUserInfo?.liveRoom\n const userInfo = sigi.LiveRoom?.liveRoomUserInfo?.user\n\n if (!liveRoom || !userInfo?.roomId) return null\n\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) {\n streamUrl = findStreamUrlRecursively(sigi)\n }\n if (!streamUrl) {\n streamUrl = await fetchStreamUrlFromApi(roomId)\n }\n }\n\n return { roomId, isLive, streamUrl, title: liveRoom.title ?? null }\n } catch (_err) {\n // Timeout (15s) or network error → treat as offline\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 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}\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, ~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(\n sigi: SigiState,\n user: string,\n ): 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(\n userId: string,\n ): 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(\n /<script\\s+id=\"SIGI_STATE\"\\s+type=\"application\\/json\">(.*?)<\\/script>/,\n );\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<\n NonNullable<SigiState[\"LiveRoom\"]>[\"liveRoomUserInfo\"]\n >[\"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\n | Record<string, unknown>\n | 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