@postfetch/core 0.5.1 → 0.6.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/README.md +1 -1
- package/dist/fingerprint.d.ts +5 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +400 -45
- package/dist/internal.d.ts +2 -2
- package/dist/linkedin.d.ts +3 -0
- package/dist/postfetch.d.ts +1 -1
- package/package.json +3 -2
- package/src/facebook.ts +40 -3
- package/src/fingerprint.ts +21 -7
- package/src/index.ts +1 -1
- package/src/instagram.ts +54 -22
- package/src/internal.ts +2 -1
- package/src/linkedin.ts +243 -0
- package/src/postfetch.ts +8 -2
- package/src/tiktok.ts +121 -16
package/src/tiktok.ts
CHANGED
|
@@ -14,27 +14,26 @@ import {
|
|
|
14
14
|
type PostfetchResult,
|
|
15
15
|
type MediaItem,
|
|
16
16
|
} from "./internal";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
browserUserAgent,
|
|
19
|
+
firefoxNavigationHeaders,
|
|
20
|
+
navigationHeaders,
|
|
21
|
+
} from "./fingerprint";
|
|
18
22
|
|
|
19
23
|
const marker = '<script id="__UNIVERSAL_DATA_FOR_REHYDRATION__" type="application/json">';
|
|
24
|
+
const pageAttemptCount = 12;
|
|
20
25
|
|
|
21
26
|
export async function resolveTiktok(input: ResolveContext): Promise<PostfetchResult> {
|
|
22
|
-
const
|
|
23
|
-
const pageUrl = await followShortlink(input.net, userAgent, input.url);
|
|
27
|
+
const pageUrl = await followShortlink(input.net, browserUserAgent(), input.url);
|
|
24
28
|
const id = videoId(pageUrl);
|
|
25
29
|
if (!id) {
|
|
26
30
|
throw new Error("TikTok video id not found");
|
|
27
31
|
}
|
|
28
|
-
const page = await
|
|
29
|
-
if (!recoverablePageError(error)) {
|
|
30
|
-
throw error;
|
|
31
|
-
}
|
|
32
|
-
return fetchVideoPage(input.net, id, firefoxUserAgent());
|
|
33
|
-
});
|
|
32
|
+
const page = await videoPage(input.net, id, asUrl(pageUrl).pathname.includes("/photo/"));
|
|
34
33
|
const user = author(page.item) ?? username(pageUrl) ?? "i";
|
|
35
34
|
const headers: Record<string, string> = {
|
|
36
35
|
referer: `https://www.tiktok.com/@${encodeURIComponent(user)}/video/${encodeURIComponent(id)}`,
|
|
37
|
-
"user-agent": userAgent,
|
|
36
|
+
"user-agent": page.userAgent,
|
|
38
37
|
};
|
|
39
38
|
if (page.cookie) {
|
|
40
39
|
headers.cookie = page.cookie;
|
|
@@ -46,6 +45,41 @@ export async function resolveTiktok(input: ResolveContext): Promise<PostfetchRes
|
|
|
46
45
|
return { archiveFilename: filename(`tiktok_${user}_${id}.zip`), id, items, metadata: tiktokMetadata(page.item), platform: "tiktok" };
|
|
47
46
|
}
|
|
48
47
|
|
|
48
|
+
async function videoPage(
|
|
49
|
+
net: Net,
|
|
50
|
+
id: string,
|
|
51
|
+
photo: boolean,
|
|
52
|
+
): Promise<{ item: Json; cookie: string | null; userAgent: string }> {
|
|
53
|
+
for (let attempt = 0; attempt < pageAttemptCount; attempt += 1) {
|
|
54
|
+
const baseHeaders = attempt % 2 === 0 ? navigationHeaders() : firefoxNavigationHeaders();
|
|
55
|
+
const userAgent = baseHeaders["user-agent"];
|
|
56
|
+
const headers = {
|
|
57
|
+
...baseHeaders,
|
|
58
|
+
referer: "https://www.tiktok.com/",
|
|
59
|
+
"sec-fetch-site": "same-origin",
|
|
60
|
+
};
|
|
61
|
+
try {
|
|
62
|
+
return { ...(await fetchVideoPage(net, id, headers)), userAgent };
|
|
63
|
+
} catch (error) {
|
|
64
|
+
if (!recoverablePageError(error)) {
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!photo) {
|
|
71
|
+
throw new Error("TikTok hydration not found");
|
|
72
|
+
}
|
|
73
|
+
const baseHeaders = navigationHeaders();
|
|
74
|
+
const userAgent = baseHeaders["user-agent"];
|
|
75
|
+
const headers = {
|
|
76
|
+
...baseHeaders,
|
|
77
|
+
referer: "https://www.tiktok.com/",
|
|
78
|
+
"sec-fetch-site": "same-origin",
|
|
79
|
+
};
|
|
80
|
+
return { ...(await fetchEmbedPage(net, id, headers)), userAgent };
|
|
81
|
+
}
|
|
82
|
+
|
|
49
83
|
export function tiktokMetadata(item: Json): PostMetadata & { extra?: TiktokExtra } {
|
|
50
84
|
const user = object(item.author) ? item.author : null;
|
|
51
85
|
const stats = object(item.stats) ? item.stats : null;
|
|
@@ -78,18 +112,25 @@ export function tiktokMetadata(item: Json): PostMetadata & { extra?: TiktokExtra
|
|
|
78
112
|
async function fetchVideoPage(
|
|
79
113
|
net: Net,
|
|
80
114
|
id: string,
|
|
81
|
-
|
|
115
|
+
headers: Record<string, string>,
|
|
82
116
|
): Promise<{ item: Json; cookie: string | null }> {
|
|
83
117
|
const page = await net(`https://www.tiktok.com/@i/video/${id}`, {
|
|
84
|
-
headers
|
|
85
|
-
accept: "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8",
|
|
86
|
-
referer: "https://www.tiktok.com/",
|
|
87
|
-
"user-agent": userAgent,
|
|
88
|
-
},
|
|
118
|
+
headers,
|
|
89
119
|
});
|
|
90
120
|
return { cookie: cookieHeader(page.headers), item: itemStruct(await page.text()) };
|
|
91
121
|
}
|
|
92
122
|
|
|
123
|
+
async function fetchEmbedPage(
|
|
124
|
+
net: Net,
|
|
125
|
+
id: string,
|
|
126
|
+
headers: Record<string, string>,
|
|
127
|
+
): Promise<{ item: Json; cookie: string | null }> {
|
|
128
|
+
const page = await net(`https://www.tiktok.com/embed/v2/${id}`, {
|
|
129
|
+
headers,
|
|
130
|
+
});
|
|
131
|
+
return { cookie: cookieHeader(page.headers), item: embedItemStruct(await page.text(), id) };
|
|
132
|
+
}
|
|
133
|
+
|
|
93
134
|
export function isShortlinkHost(hostname: string): boolean {
|
|
94
135
|
return hostname === "vm.tiktok.com" || hostname === "vt.tiktok.com";
|
|
95
136
|
}
|
|
@@ -134,11 +175,75 @@ function itemStruct(html: string): Json {
|
|
|
134
175
|
return item;
|
|
135
176
|
}
|
|
136
177
|
|
|
178
|
+
function embedItemStruct(html: string, id: string): Json {
|
|
179
|
+
const raw = html.match(/<script\b[^>]*\bid=["']__FRONTITY_CONNECT_STATE__["'][^>]*>([\s\S]*?)<\/script>/i)?.[1];
|
|
180
|
+
if (!raw) {
|
|
181
|
+
throw new Error("TikTok embed state not found");
|
|
182
|
+
}
|
|
183
|
+
const parsed: unknown = JSON.parse(raw);
|
|
184
|
+
const source = object(parsed) && object(parsed.source) ? parsed.source : null;
|
|
185
|
+
const data = source && object(source.data) ? source.data : null;
|
|
186
|
+
const rawRoute = data ? data[`/embed/v2/${id}`] : null;
|
|
187
|
+
const route = object(rawRoute) ? rawRoute : null;
|
|
188
|
+
const videoData = route && object(route.videoData) ? route.videoData : null;
|
|
189
|
+
const item = videoData && object(videoData.itemInfos) ? videoData.itemInfos : null;
|
|
190
|
+
if (!videoData || !item) {
|
|
191
|
+
throw new Error("TikTok embed item not found");
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const authorInfo = object(videoData.authorInfos) ? videoData.authorInfos : {};
|
|
195
|
+
const musicInfo = object(videoData.musicInfos) ? videoData.musicInfos : {};
|
|
196
|
+
const video = object(item.video) ? item.video : {};
|
|
197
|
+
const videoMeta = object(video.videoMeta) ? video.videoMeta : {};
|
|
198
|
+
const imagePost = object(videoData.imagePostInfo) ? videoData.imagePostInfo : null;
|
|
199
|
+
const displayImages = imagePost && Array.isArray(imagePost.displayImages) ? imagePost.displayImages.filter(object) : [];
|
|
200
|
+
if (displayImages.length === 0) {
|
|
201
|
+
throw new Error("TikTok video hydration not found");
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return {
|
|
205
|
+
...item,
|
|
206
|
+
author: {
|
|
207
|
+
nickname: string(authorInfo.nickName),
|
|
208
|
+
uniqueId: string(authorInfo.uniqueId),
|
|
209
|
+
verified: bool(authorInfo.verified),
|
|
210
|
+
},
|
|
211
|
+
desc: string(item.text),
|
|
212
|
+
imagePost: {
|
|
213
|
+
images: displayImages.map((image) => ({
|
|
214
|
+
imageURL: {
|
|
215
|
+
urlList: Array.isArray(image.urlList) ? image.urlList.map(string).filter((url): url is string => Boolean(url)) : [],
|
|
216
|
+
},
|
|
217
|
+
})),
|
|
218
|
+
},
|
|
219
|
+
music: {
|
|
220
|
+
authorName: string(musicInfo.authorName),
|
|
221
|
+
playUrl: firstString(musicInfo.playUrl),
|
|
222
|
+
title: string(musicInfo.musicName),
|
|
223
|
+
},
|
|
224
|
+
stats: {
|
|
225
|
+
commentCount: item.commentCount,
|
|
226
|
+
diggCount: item.diggCount,
|
|
227
|
+
playCount: item.playCount,
|
|
228
|
+
shareCount: item.shareCount,
|
|
229
|
+
},
|
|
230
|
+
video: {
|
|
231
|
+
duration: videoMeta.duration,
|
|
232
|
+
},
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
137
236
|
function recoverablePageError(error: unknown): boolean {
|
|
138
237
|
const message = error instanceof Error ? error.message : "";
|
|
139
238
|
return message === "TikTok WAF challenge" || message === "TikTok hydration not found";
|
|
140
239
|
}
|
|
141
240
|
|
|
241
|
+
function firstString(value: unknown): string | null {
|
|
242
|
+
return Array.isArray(value)
|
|
243
|
+
? value.map(string).find((candidate): candidate is string => candidate !== null) ?? null
|
|
244
|
+
: string(value);
|
|
245
|
+
}
|
|
246
|
+
|
|
142
247
|
function downloadUrl(item: Json): string | null {
|
|
143
248
|
const video = object(item.video) ? item.video : null;
|
|
144
249
|
return video ? string(video.playAddr) ?? string(video.downloadAddr) : null;
|