@lmna22/aio-downloader 2.0.1 → 2.0.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.
- package/README.md +32 -1
- package/package.json +2 -9
- package/src/index.js +4 -0
- package/src/lib/xiaohongshu.js +288 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lmna22/aio-downloader
|
|
2
2
|
|
|
3
|
-
> All-in-one media downloader for YouTube, Instagram, TikTok, Pinterest, Pixiv, X/Twitter, and
|
|
3
|
+
> All-in-one media downloader for YouTube, Instagram, TikTok, Pinterest, Pixiv, X/Twitter, Lahelu, and Xiaohongshu/RedNote.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@lmna22/aio-downloader)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -21,6 +21,7 @@ Scrape and download videos, audio, and images from multiple platforms with a sin
|
|
|
21
21
|
| **Pixiv** | — | — | ✅ (Original Resolution) | ✅ |
|
|
22
22
|
| **X / Twitter** | ✅ (Best quality) | — | ✅ | — |
|
|
23
23
|
| **Lahelu** | ✅ | — | ✅ | — |
|
|
24
|
+
| **Xiaohongshu/RedNote** | ✅ | — | ✅ | — |
|
|
24
25
|
|
|
25
26
|
- 🔗 **Auto-detect platform** from URL — just pass any supported link
|
|
26
27
|
- 📦 **Programmatic API** — designed for Node.js applications, bots, and scripts
|
|
@@ -95,6 +96,27 @@ const { lmna } = require("@lmna22/aio-downloader");
|
|
|
95
96
|
const result = await lmna.tiktok("https://www.tiktok.com/@user/video/1234567890");
|
|
96
97
|
```
|
|
97
98
|
|
|
99
|
+
### Xiaohongshu / RedNote
|
|
100
|
+
|
|
101
|
+
```javascript
|
|
102
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
103
|
+
|
|
104
|
+
const result = await lmna.xiaohongshu("https://www.xiaohongshu.com/explore/abc123");
|
|
105
|
+
|
|
106
|
+
if (result.status) {
|
|
107
|
+
console.log(result.data.title);
|
|
108
|
+
console.log(result.data.author.nickname);
|
|
109
|
+
console.log(result.data.stats.likes);
|
|
110
|
+
console.log(result.data.stats.collects);
|
|
111
|
+
console.log(result.data.stats.comments);
|
|
112
|
+
console.log(result.data.media.url);
|
|
113
|
+
|
|
114
|
+
// Download the media
|
|
115
|
+
const { download } = require("@lmna22/aio-downloader");
|
|
116
|
+
await download(result.data.media.url, `./downloads/${result.data.fileName}`);
|
|
117
|
+
}
|
|
118
|
+
```
|
|
119
|
+
|
|
98
120
|
*(See the API Reference below for other platforms: Pinterest, Pixiv, Twitter, Lahelu)*
|
|
99
121
|
|
|
100
122
|
---
|
|
@@ -132,6 +154,9 @@ await aioDownloader("https://www.pinterest.com/pin/456/");
|
|
|
132
154
|
await aioDownloader("https://www.pixiv.net/artworks/789");
|
|
133
155
|
await aioDownloader("https://x.com/user/status/101112");
|
|
134
156
|
await aioDownloader("https://lahelu.com/post/abc123");
|
|
157
|
+
await aioDownloader("https://www.xiaohongshu.com/explore/abc123");
|
|
158
|
+
await aioDownloader("https://www.rednote.com/explore/abc123");
|
|
159
|
+
await aioDownloader("https://xhslink.com/abc123");
|
|
135
160
|
|
|
136
161
|
// Force a specific platform
|
|
137
162
|
await aioDownloader("https://example.com/video", { platform: "youtube", quality: 5 });
|
|
@@ -145,6 +170,9 @@ detectPlatform("https://pin.it/abc123"); // "pinterest"
|
|
|
145
170
|
detectPlatform("https://pixiv.net/artworks/456"); // "pixiv"
|
|
146
171
|
detectPlatform("https://x.com/user/status/789"); // "twitter"
|
|
147
172
|
detectPlatform("https://lahelu.com/post/abc"); // "lahelu"
|
|
173
|
+
detectPlatform("https://xiaohongshu.com/explore/abc"); // "xiaohongshu"
|
|
174
|
+
detectPlatform("https://rednote.com/explore/abc"); // "xiaohongshu"
|
|
175
|
+
detectPlatform("https://xhslink.com/abc"); // "xiaohongshu"
|
|
148
176
|
detectPlatform("https://unknown.com"); // null
|
|
149
177
|
```
|
|
150
178
|
|
|
@@ -167,6 +195,9 @@ await lmna.pinterest(url, options);
|
|
|
167
195
|
await lmna.pixiv(url, options);
|
|
168
196
|
await lmna.twitter(url);
|
|
169
197
|
await lmna.lahelu(url);
|
|
198
|
+
await lmna.xiaohongshu(url);
|
|
199
|
+
|
|
200
|
+
// All return: { status, platform, data?, message? }
|
|
170
201
|
```
|
|
171
202
|
|
|
172
203
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmna22/aio-downloader",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.2",
|
|
4
4
|
"description": "All-in-one media downloader for YouTube, Instagram, TikTok, Pinterest, Pixiv, and X/Twitter. Scrape and download videos, audio, and images from multiple platforms with a single library.",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -37,18 +37,11 @@
|
|
|
37
37
|
"axios-cookiejar-support": "^5.0.0",
|
|
38
38
|
"cheerio": "^1.0.0",
|
|
39
39
|
"ffmpeg-static": "^5.0.0",
|
|
40
|
+
"puppeteer": "^24.0.0",
|
|
40
41
|
"qs": "^6.13.0",
|
|
41
42
|
"tough-cookie": "^5.1.0",
|
|
42
43
|
"youtube-dl-exec": "^3.1.4"
|
|
43
44
|
},
|
|
44
|
-
"peerDependencies": {
|
|
45
|
-
"puppeteer": ">=20.0.0"
|
|
46
|
-
},
|
|
47
|
-
"peerDependenciesMeta": {
|
|
48
|
-
"puppeteer": {
|
|
49
|
-
"optional": true
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
45
|
"engines": {
|
|
53
46
|
"node": ">=14.0.0"
|
|
54
47
|
}
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,7 @@ const pinterestDownloader = require("./lib/pinterest");
|
|
|
5
5
|
const pixivDownloader = require("./lib/pixiv");
|
|
6
6
|
const twitterDownloader = require("./lib/twitter");
|
|
7
7
|
const laheluDownloader = require("./lib/lahelu");
|
|
8
|
+
const xiaohongshuDownloader = require("./lib/xiaohongshu");
|
|
8
9
|
const download = require("./download");
|
|
9
10
|
|
|
10
11
|
const PLATFORM_PATTERNS = [
|
|
@@ -15,6 +16,7 @@ const PLATFORM_PATTERNS = [
|
|
|
15
16
|
{ name: "pixiv", test: (url) => /pixiv\.net\//i.test(url) },
|
|
16
17
|
{ name: "twitter", test: (url) => /(?:twitter\.com\/|x\.com\/)/i.test(url) },
|
|
17
18
|
{ name: "lahelu", test: (url) => /lahelu\.com\/post\//i.test(url) },
|
|
19
|
+
{ name: "xiaohongshu", test: (url) => /xiaohongshu\.com|rednote\.com|xhslink\.com/i.test(url) },
|
|
18
20
|
];
|
|
19
21
|
|
|
20
22
|
function detectPlatform(url) {
|
|
@@ -32,6 +34,7 @@ const scrapers = {
|
|
|
32
34
|
pixiv: (url, options) => pixivDownloader(url, options),
|
|
33
35
|
twitter: (url) => twitterDownloader(url),
|
|
34
36
|
lahelu: (url) => laheluDownloader(url),
|
|
37
|
+
xiaohongshu: (url) => xiaohongshuDownloader(url),
|
|
35
38
|
};
|
|
36
39
|
|
|
37
40
|
async function aioDownloader(url, options = {}) {
|
|
@@ -60,6 +63,7 @@ const lmna = {
|
|
|
60
63
|
pixiv: (url, options) => pixivDownloader(url, options),
|
|
61
64
|
twitter: (url) => twitterDownloader(url),
|
|
62
65
|
lahelu: (url) => laheluDownloader(url),
|
|
66
|
+
xiaohongshu: (url) => xiaohongshuDownloader(url),
|
|
63
67
|
};
|
|
64
68
|
|
|
65
69
|
module.exports = {
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
const { sanitizeFileName, getExtFromUrl } = require("../utils");
|
|
4
|
+
|
|
5
|
+
let puppeteer;
|
|
6
|
+
try {
|
|
7
|
+
puppeteer = require("puppeteer");
|
|
8
|
+
} catch (err) { }
|
|
9
|
+
|
|
10
|
+
function isXiaohongshuUrl(url) {
|
|
11
|
+
try {
|
|
12
|
+
const u = new URL(url);
|
|
13
|
+
return u.hostname.includes("xiaohongshu.com") ||
|
|
14
|
+
u.hostname.includes("rednote.com") ||
|
|
15
|
+
u.hostname.includes("xhslink.com");
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseNoteId(url) {
|
|
22
|
+
const patterns = [
|
|
23
|
+
/xiaohongshu\.com\/(?:explore|discovery\/item)\/([a-f0-9]+)/i,
|
|
24
|
+
/rednote\.com\/(?:explore|discovery\/item)\/([a-f0-9]+)/i,
|
|
25
|
+
/xhslink\.com\/([a-zA-Z0-9]+)/i,
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
for (const pattern of patterns) {
|
|
29
|
+
const match = url.match(pattern);
|
|
30
|
+
if (match) return match[1];
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildPostUrl(noteIdOrUrl) {
|
|
36
|
+
if (noteIdOrUrl.startsWith("http")) return noteIdOrUrl;
|
|
37
|
+
return `https://www.xiaohongshu.com/explore/${noteIdOrUrl}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function resolveShortLink(url) {
|
|
41
|
+
if (!url.includes("xhslink.com")) return url;
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
const res = await axios.head(url, {
|
|
45
|
+
maxRedirects: 5,
|
|
46
|
+
headers: {
|
|
47
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36",
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
return res.request.res.responseUrl || url;
|
|
51
|
+
} catch (e) {
|
|
52
|
+
if (e.response && e.response.headers && e.response.headers.location) {
|
|
53
|
+
return e.response.headers.location;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return url;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function fetchXiaohongshuData(url) {
|
|
60
|
+
if (!puppeteer) {
|
|
61
|
+
throw new Error("Puppeteer is required for Xiaohongshu downloads. Install it with: npm install puppeteer");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let browser = null;
|
|
65
|
+
try {
|
|
66
|
+
browser = await puppeteer.launch({
|
|
67
|
+
headless: "new",
|
|
68
|
+
args: [
|
|
69
|
+
"--disable-blink-features=AutomationControlled",
|
|
70
|
+
"--disable-web-security",
|
|
71
|
+
"--disable-features=IsolateOrigins,site-per-process",
|
|
72
|
+
"--window-size=1920,1080",
|
|
73
|
+
"--no-sandbox",
|
|
74
|
+
"--disable-setuid-sandbox",
|
|
75
|
+
],
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const page = await browser.newPage();
|
|
79
|
+
await page.setViewport({ width: 1920, height: 1080 });
|
|
80
|
+
|
|
81
|
+
await page.setUserAgent(
|
|
82
|
+
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36"
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
await page.evaluateOnNewDocument(() => {
|
|
86
|
+
Object.defineProperty(navigator, "webdriver", { get: () => undefined });
|
|
87
|
+
Object.defineProperty(navigator, "plugins", { get: () => [1, 2, 3, 4, 5] });
|
|
88
|
+
window.chrome = { runtime: {} };
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
page.setDefaultTimeout(30000);
|
|
92
|
+
|
|
93
|
+
let finalUrl = await resolveShortLink(url);
|
|
94
|
+
|
|
95
|
+
await page.goto(finalUrl, {
|
|
96
|
+
waitUntil: "networkidle2",
|
|
97
|
+
timeout: 30000,
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
await page.waitForFunction(
|
|
101
|
+
() => window.__INITIAL_STATE__ !== undefined,
|
|
102
|
+
{ timeout: 15000 }
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
await new Promise((r) => setTimeout(r, 1500));
|
|
106
|
+
|
|
107
|
+
const postData = await page.evaluate(() => {
|
|
108
|
+
const state = window.__INITIAL_STATE__;
|
|
109
|
+
if (!state) return null;
|
|
110
|
+
|
|
111
|
+
const noteMap = state?.note?.noteDetailMap || {};
|
|
112
|
+
const firstKey = Object.keys(noteMap)[0];
|
|
113
|
+
const noteWrapper = noteMap[firstKey];
|
|
114
|
+
const note = noteWrapper?.note || noteWrapper;
|
|
115
|
+
|
|
116
|
+
if (!note) return null;
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
id: note.noteId || note.id || firstKey || "",
|
|
120
|
+
title: note.title || "",
|
|
121
|
+
desc: note.desc || "",
|
|
122
|
+
type: note.type || "normal",
|
|
123
|
+
author: {
|
|
124
|
+
nickname: note.user?.nickname || "",
|
|
125
|
+
userId: note.user?.userId || "",
|
|
126
|
+
avatar: note.user?.image || note.user?.avatar || "",
|
|
127
|
+
},
|
|
128
|
+
images: (note.imageList || []).map((img) => ({
|
|
129
|
+
url: img.urlDefault || img.url || "",
|
|
130
|
+
livePhoto: img.livePhoto || img.stream?.h264?.[0]?.masterUrl || "",
|
|
131
|
+
width: img.width || 0,
|
|
132
|
+
height: img.height || 0,
|
|
133
|
+
})),
|
|
134
|
+
video: note.video ? {
|
|
135
|
+
url: note.video.media?.stream?.h264?.[0]?.masterUrl ||
|
|
136
|
+
note.video.media?.stream?.h265?.[0]?.masterUrl ||
|
|
137
|
+
note.video.url || "",
|
|
138
|
+
backupUrl: note.video.media?.stream?.h264?.[0]?.backupUrls?.[0] ||
|
|
139
|
+
note.video.media?.stream?.h265?.[0]?.backupUrls?.[0] || "",
|
|
140
|
+
duration: note.video.duration || 0,
|
|
141
|
+
width: note.video.width || note.video.media?.stream?.h264?.[0]?.width || 0,
|
|
142
|
+
height: note.video.height || note.video.media?.stream?.h264?.[0]?.height || 0,
|
|
143
|
+
cover: note.video.image?.firstFrameFileid || note.video.thumbnail || "",
|
|
144
|
+
} : null,
|
|
145
|
+
cover: note.cover?.urlDefault || note.cover?.url || "",
|
|
146
|
+
stats: {
|
|
147
|
+
likes: note.interactInfo?.likedCount || "0",
|
|
148
|
+
collects: note.interactInfo?.collectedCount || "0",
|
|
149
|
+
comments: note.interactInfo?.commentCount || "0",
|
|
150
|
+
shares: note.interactInfo?.shareCount || "0",
|
|
151
|
+
},
|
|
152
|
+
tags: (note.tagList || []).map((tag) => tag.name || "").filter(Boolean),
|
|
153
|
+
publishedAt: note.time || "",
|
|
154
|
+
ipLocation: note.ipLocation || "",
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
await browser.close();
|
|
159
|
+
browser = null;
|
|
160
|
+
|
|
161
|
+
if (!postData) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const medias = [];
|
|
166
|
+
|
|
167
|
+
if (postData.video && postData.video.url) {
|
|
168
|
+
medias.push({
|
|
169
|
+
type: "video",
|
|
170
|
+
format: "mp4",
|
|
171
|
+
desc: `Video ${postData.video.width}x${postData.video.height}` +
|
|
172
|
+
(postData.video.duration ? ` (${Math.round(postData.video.duration)}s)` : ""),
|
|
173
|
+
url: postData.video.url,
|
|
174
|
+
backupUrl: postData.video.backupUrl || "",
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (postData.images && postData.images.length > 0) {
|
|
179
|
+
postData.images.forEach((img, i) => {
|
|
180
|
+
if (img.url) {
|
|
181
|
+
medias.push({
|
|
182
|
+
type: "image",
|
|
183
|
+
format: "jpg",
|
|
184
|
+
desc: `Image ${i + 1}` + (img.width && img.height ? ` (${img.width}x${img.height})` : ""),
|
|
185
|
+
url: img.url.startsWith("http") ? img.url : `https://sns-img-bd.xhscdn.com/${img.url}`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
if (img.livePhoto) {
|
|
189
|
+
medias.push({
|
|
190
|
+
type: "video",
|
|
191
|
+
format: "mp4",
|
|
192
|
+
desc: `Live Photo ${i + 1}`,
|
|
193
|
+
url: img.livePhoto,
|
|
194
|
+
});
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
return {
|
|
200
|
+
...postData,
|
|
201
|
+
medias,
|
|
202
|
+
};
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (browser) {
|
|
205
|
+
try { await browser.close(); } catch (_) { }
|
|
206
|
+
}
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function xiaohongshuDownloader(url) {
|
|
212
|
+
try {
|
|
213
|
+
if (!isXiaohongshuUrl(url)) {
|
|
214
|
+
return {
|
|
215
|
+
status: false,
|
|
216
|
+
platform: "xiaohongshu",
|
|
217
|
+
message: "Invalid Xiaohongshu URL. Make sure it's a valid post URL.",
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
const noteId = parseNoteId(url);
|
|
222
|
+
if (!noteId && !url.includes("xhslink.com")) {
|
|
223
|
+
return {
|
|
224
|
+
status: false,
|
|
225
|
+
platform: "xiaohongshu",
|
|
226
|
+
message: "Could not extract note ID from URL.",
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const postUrl = buildPostUrl(url);
|
|
231
|
+
const data = await fetchXiaohongshuData(postUrl);
|
|
232
|
+
|
|
233
|
+
if (!data || !data.medias || data.medias.length === 0) {
|
|
234
|
+
return {
|
|
235
|
+
status: false,
|
|
236
|
+
platform: "xiaohongshu",
|
|
237
|
+
message: "No media found in this post. The post may be private or deleted.",
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const selected = data.medias[0];
|
|
242
|
+
const ext = selected.format.startsWith(".") ? selected.format : "." + selected.format;
|
|
243
|
+
const safeTitle = sanitizeFileName(data.id || noteId || "xhs");
|
|
244
|
+
const fileName = `xhs_${safeTitle}_${selected.type}${ext}`;
|
|
245
|
+
|
|
246
|
+
return {
|
|
247
|
+
status: true,
|
|
248
|
+
platform: "xiaohongshu",
|
|
249
|
+
data: {
|
|
250
|
+
id: data.id || noteId,
|
|
251
|
+
title: data.title || "Untitled",
|
|
252
|
+
description: data.desc || "",
|
|
253
|
+
author: {
|
|
254
|
+
nickname: data.author?.nickname || "Unknown",
|
|
255
|
+
userId: data.author?.userId || "",
|
|
256
|
+
avatar: data.author?.avatar || "",
|
|
257
|
+
},
|
|
258
|
+
type: data.type,
|
|
259
|
+
stats: {
|
|
260
|
+
likes: data.stats?.likes || "0",
|
|
261
|
+
collects: data.stats?.collects || "0",
|
|
262
|
+
comments: data.stats?.comments || "0",
|
|
263
|
+
shares: data.stats?.shares || "0",
|
|
264
|
+
},
|
|
265
|
+
tags: data.tags || [],
|
|
266
|
+
publishedAt: data.publishedAt,
|
|
267
|
+
ipLocation: data.ipLocation,
|
|
268
|
+
media: {
|
|
269
|
+
type: selected.type,
|
|
270
|
+
format: selected.format,
|
|
271
|
+
url: selected.url,
|
|
272
|
+
backupUrl: selected.backupUrl || "",
|
|
273
|
+
desc: selected.desc,
|
|
274
|
+
},
|
|
275
|
+
allMedias: data.medias,
|
|
276
|
+
fileName: fileName,
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
} catch (error) {
|
|
280
|
+
return {
|
|
281
|
+
status: false,
|
|
282
|
+
platform: "xiaohongshu",
|
|
283
|
+
message: error.message || "An unexpected error occurred",
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
module.exports = xiaohongshuDownloader;
|