@postfetch/core 0.13.1 → 0.13.3
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 +2 -0
- package/dist/index.js +40 -21
- package/package.json +1 -1
- package/src/linkedin.ts +34 -16
- package/src/postfetch.ts +1 -1
- package/src/reddit.ts +9 -4
package/README.md
CHANGED
|
@@ -13,6 +13,8 @@ for (const item of result.items) {
|
|
|
13
13
|
|
|
14
14
|
`postfetch(url, options?)` detects the platform and resolves its media into a typed `PostfetchResult` (URLs plus the headers needed to fetch them). It performs no side effects beyond the lookup — `download`, `downloadBlob`, `archive` and `toResponse` materialize the result.
|
|
15
15
|
|
|
16
|
+
LinkedIn public posts also accept `lnkd.in` shortlinks, including `https://lnkd.in/p/…`.
|
|
17
|
+
|
|
16
18
|
Spotify track URLs are matched to YouTube by artist, title and duration, preferring
|
|
17
19
|
artist uploads and rejecting recognizable alternate versions unless requested.
|
|
18
20
|
Results have `platform: "spotify"`, Spotify metadata and
|
package/dist/index.js
CHANGED
|
@@ -796,21 +796,10 @@ function randomToken(length = 8) {
|
|
|
796
796
|
|
|
797
797
|
// src/linkedin.ts
|
|
798
798
|
async function resolveLinkedin(input) {
|
|
799
|
-
const reference = postReference(input
|
|
799
|
+
const reference = await postReference(input);
|
|
800
800
|
const pageUrl = `https://www.linkedin.com/feed/update/urn:li:${reference.kind}:${reference.id}`;
|
|
801
801
|
const response = await input.net(pageUrl, { headers: navigationHeaders() });
|
|
802
|
-
|
|
803
|
-
throw new PostfetchError(404, "LinkedIn post not found", "notFound");
|
|
804
|
-
}
|
|
805
|
-
if (response.status === 429 || response.status === 999) {
|
|
806
|
-
throw new PostfetchError(429, "LinkedIn rate limit reached", "rateLimited");
|
|
807
|
-
}
|
|
808
|
-
if (!response.ok) {
|
|
809
|
-
throw new PostfetchError(response.status, "LinkedIn post unavailable", "unavailable");
|
|
810
|
-
}
|
|
811
|
-
if (response.url.includes("/signup/") || response.url.includes("/login")) {
|
|
812
|
-
throw new PostfetchError(401, "LinkedIn login required", "loginRequired");
|
|
813
|
-
}
|
|
802
|
+
checkResponse(response);
|
|
814
803
|
const html = await response.text();
|
|
815
804
|
const post = linkedinPost(html);
|
|
816
805
|
if (!post) {
|
|
@@ -846,8 +835,31 @@ function linkedinMetadata(post) {
|
|
|
846
835
|
commentCount: count(post.commentCount) ?? interactionCount(post.interactionStatistic, "CommentAction")
|
|
847
836
|
};
|
|
848
837
|
}
|
|
849
|
-
function
|
|
850
|
-
|
|
838
|
+
function checkResponse(response) {
|
|
839
|
+
if (response.status === 404) {
|
|
840
|
+
throw new PostfetchError(404, "LinkedIn post not found", "notFound");
|
|
841
|
+
}
|
|
842
|
+
if (response.status === 429 || response.status === 999) {
|
|
843
|
+
throw new PostfetchError(429, "LinkedIn rate limit reached", "rateLimited");
|
|
844
|
+
}
|
|
845
|
+
if (!response.ok) {
|
|
846
|
+
throw new PostfetchError(response.status, "LinkedIn post unavailable", "unavailable");
|
|
847
|
+
}
|
|
848
|
+
if (response.url.includes("/signup/") || response.url.includes("/login")) {
|
|
849
|
+
throw new PostfetchError(401, "LinkedIn login required", "loginRequired");
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
async function postReference(input) {
|
|
853
|
+
let url = input.url;
|
|
854
|
+
if (new URL(url).hostname === "lnkd.in") {
|
|
855
|
+
const response = await input.net(url, { headers: navigationHeaders(), redirect: "manual" });
|
|
856
|
+
await response.body?.cancel();
|
|
857
|
+
if (![301, 302, 303, 307, 308].includes(response.status)) {
|
|
858
|
+
checkResponse(response);
|
|
859
|
+
}
|
|
860
|
+
url = response.headers.get("location") ?? "";
|
|
861
|
+
}
|
|
862
|
+
const reference = postReferenceOrNull(url);
|
|
851
863
|
if (!reference) {
|
|
852
864
|
throw new PostfetchError(400, "LinkedIn post id not found");
|
|
853
865
|
}
|
|
@@ -856,7 +868,11 @@ function postReference(input) {
|
|
|
856
868
|
function postReferenceOrNull(input) {
|
|
857
869
|
let pathname;
|
|
858
870
|
try {
|
|
859
|
-
|
|
871
|
+
const url = new URL(input);
|
|
872
|
+
if (url.hostname !== "linkedin.com" && !url.hostname.endsWith(".linkedin.com")) {
|
|
873
|
+
return null;
|
|
874
|
+
}
|
|
875
|
+
pathname = url.pathname.replace(/%3A/gi, ":");
|
|
860
876
|
} catch {
|
|
861
877
|
return null;
|
|
862
878
|
}
|
|
@@ -1626,14 +1642,17 @@ async function appToken(net) {
|
|
|
1626
1642
|
return token;
|
|
1627
1643
|
}
|
|
1628
1644
|
async function fetchPost(net, token, id) {
|
|
1629
|
-
const
|
|
1630
|
-
|
|
1631
|
-
|
|
1645
|
+
const headers = { authorization: `Bearer ${token}`, "user-agent": browserUserAgent() };
|
|
1646
|
+
let response = await net(`https://oauth.reddit.com/comments/${id}?raw_json=1&limit=1`, { headers });
|
|
1647
|
+
if (response.status === 403) {
|
|
1648
|
+
await response.body?.cancel();
|
|
1649
|
+
response = await net(`https://oauth.reddit.com/by_id/t3_${id}?raw_json=1`, { headers });
|
|
1650
|
+
}
|
|
1632
1651
|
if (!response.ok) {
|
|
1633
1652
|
throw new Error(`Reddit post failed: ${response.status}`);
|
|
1634
1653
|
}
|
|
1635
1654
|
const payload = await response.json().catch(() => null);
|
|
1636
|
-
const listing = Array.isArray(payload) ? payload[0] :
|
|
1655
|
+
const listing = Array.isArray(payload) ? payload[0] : payload;
|
|
1637
1656
|
const children = object(listing) && object(listing.data) && Array.isArray(listing.data.children) ? listing.data.children : [];
|
|
1638
1657
|
const first = children[0];
|
|
1639
1658
|
const post = object(first) && object(first.data) ? first.data : null;
|
|
@@ -3348,7 +3367,7 @@ function detect(url) {
|
|
|
3348
3367
|
if (host.includes("instagram.com")) {
|
|
3349
3368
|
return "instagram";
|
|
3350
3369
|
}
|
|
3351
|
-
if (host === "linkedin.com" || host.endsWith(".linkedin.com")) {
|
|
3370
|
+
if (host === "linkedin.com" || host.endsWith(".linkedin.com") || host === "lnkd.in") {
|
|
3352
3371
|
return "linkedin";
|
|
3353
3372
|
}
|
|
3354
3373
|
if (host.includes("pinterest.") || host === "pin.it") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@postfetch/core",
|
|
3
|
-
"version": "0.13.
|
|
3
|
+
"version": "0.13.3",
|
|
4
4
|
"description": "Zero-dependency typed core that turns Facebook, Instagram, LinkedIn, Pinterest, Reddit, SoundCloud, Spotify, TikTok, X and YouTube post URLs into media files.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
package/src/linkedin.ts
CHANGED
|
@@ -21,21 +21,10 @@ type LinkedinPostReference = {
|
|
|
21
21
|
};
|
|
22
22
|
|
|
23
23
|
export async function resolveLinkedin(input: ResolveContext): Promise<PostfetchResult> {
|
|
24
|
-
const reference = postReference(input
|
|
24
|
+
const reference = await postReference(input);
|
|
25
25
|
const pageUrl = `https://www.linkedin.com/feed/update/urn:li:${reference.kind}:${reference.id}`;
|
|
26
26
|
const response = await input.net(pageUrl, { headers: navigationHeaders() });
|
|
27
|
-
|
|
28
|
-
throw new PostfetchError(404, "LinkedIn post not found", "notFound");
|
|
29
|
-
}
|
|
30
|
-
if (response.status === 429 || response.status === 999) {
|
|
31
|
-
throw new PostfetchError(429, "LinkedIn rate limit reached", "rateLimited");
|
|
32
|
-
}
|
|
33
|
-
if (!response.ok) {
|
|
34
|
-
throw new PostfetchError(response.status, "LinkedIn post unavailable", "unavailable");
|
|
35
|
-
}
|
|
36
|
-
if (response.url.includes("/signup/") || response.url.includes("/login")) {
|
|
37
|
-
throw new PostfetchError(401, "LinkedIn login required", "loginRequired");
|
|
38
|
-
}
|
|
27
|
+
checkResponse(response);
|
|
39
28
|
|
|
40
29
|
const html = await response.text();
|
|
41
30
|
const post = linkedinPost(html);
|
|
@@ -76,8 +65,33 @@ export function linkedinMetadata(post: Json): PostMetadata {
|
|
|
76
65
|
};
|
|
77
66
|
}
|
|
78
67
|
|
|
79
|
-
function
|
|
80
|
-
|
|
68
|
+
function checkResponse(response: Response): void {
|
|
69
|
+
if (response.status === 404) {
|
|
70
|
+
throw new PostfetchError(404, "LinkedIn post not found", "notFound");
|
|
71
|
+
}
|
|
72
|
+
if (response.status === 429 || response.status === 999) {
|
|
73
|
+
throw new PostfetchError(429, "LinkedIn rate limit reached", "rateLimited");
|
|
74
|
+
}
|
|
75
|
+
if (!response.ok) {
|
|
76
|
+
throw new PostfetchError(response.status, "LinkedIn post unavailable", "unavailable");
|
|
77
|
+
}
|
|
78
|
+
if (response.url.includes("/signup/") || response.url.includes("/login")) {
|
|
79
|
+
throw new PostfetchError(401, "LinkedIn login required", "loginRequired");
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function postReference(input: ResolveContext): Promise<LinkedinPostReference> {
|
|
84
|
+
let url = input.url;
|
|
85
|
+
if (new URL(url).hostname === "lnkd.in") {
|
|
86
|
+
// Read the destination without fetching the post twice or following outbound links.
|
|
87
|
+
const response = await input.net(url, { headers: navigationHeaders(), redirect: "manual" });
|
|
88
|
+
await response.body?.cancel();
|
|
89
|
+
if (![301, 302, 303, 307, 308].includes(response.status)) {
|
|
90
|
+
checkResponse(response);
|
|
91
|
+
}
|
|
92
|
+
url = response.headers.get("location") ?? "";
|
|
93
|
+
}
|
|
94
|
+
const reference = postReferenceOrNull(url);
|
|
81
95
|
if (!reference) {
|
|
82
96
|
throw new PostfetchError(400, "LinkedIn post id not found");
|
|
83
97
|
}
|
|
@@ -87,7 +101,11 @@ function postReference(input: string): LinkedinPostReference {
|
|
|
87
101
|
function postReferenceOrNull(input: string): LinkedinPostReference | null {
|
|
88
102
|
let pathname: string;
|
|
89
103
|
try {
|
|
90
|
-
|
|
104
|
+
const url = new URL(input);
|
|
105
|
+
if (url.hostname !== "linkedin.com" && !url.hostname.endsWith(".linkedin.com")) {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
pathname = url.pathname.replace(/%3A/gi, ":");
|
|
91
109
|
} catch {
|
|
92
110
|
return null;
|
|
93
111
|
}
|
package/src/postfetch.ts
CHANGED
|
@@ -171,7 +171,7 @@ export function detect(url: string): Platform {
|
|
|
171
171
|
if (host.includes("instagram.com")) {
|
|
172
172
|
return "instagram";
|
|
173
173
|
}
|
|
174
|
-
if (host === "linkedin.com" || host.endsWith(".linkedin.com")) {
|
|
174
|
+
if (host === "linkedin.com" || host.endsWith(".linkedin.com") || host === "lnkd.in") {
|
|
175
175
|
return "linkedin";
|
|
176
176
|
}
|
|
177
177
|
if (host.includes("pinterest.") || host === "pin.it") {
|
package/src/reddit.ts
CHANGED
|
@@ -100,14 +100,19 @@ async function appToken(net: Net): Promise<string> {
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
async function fetchPost(net: Net, token: string, id: string): Promise<Json> {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
103
|
+
const headers = { authorization: `Bearer ${token}`, "user-agent": browserUserAgent() };
|
|
104
|
+
let response = await net(`https://oauth.reddit.com/comments/${id}?raw_json=1&limit=1`, { headers });
|
|
105
|
+
// Reddit can block the comments route while still allowing direct post
|
|
106
|
+
// listings with the same token. This route returns the post, not its replies.
|
|
107
|
+
if (response.status === 403) {
|
|
108
|
+
await response.body?.cancel();
|
|
109
|
+
response = await net(`https://oauth.reddit.com/by_id/t3_${id}?raw_json=1`, { headers });
|
|
110
|
+
}
|
|
106
111
|
if (!response.ok) {
|
|
107
112
|
throw new Error(`Reddit post failed: ${response.status}`);
|
|
108
113
|
}
|
|
109
114
|
const payload = await response.json().catch(() => null);
|
|
110
|
-
const listing = Array.isArray(payload) ? payload[0] :
|
|
115
|
+
const listing = Array.isArray(payload) ? payload[0] : payload;
|
|
111
116
|
const children = object(listing) && object(listing.data) && Array.isArray(listing.data.children) ? listing.data.children : [];
|
|
112
117
|
const first = children[0];
|
|
113
118
|
const post = object(first) && object(first.data) ? first.data : null;
|