@lmna22/aio-downloader 2.0.0 → 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 +41 -293
- package/package.json +3 -10
- package/src/index.js +4 -9
- package/src/lib/lahelu.js +37 -42
- package/src/lib/xiaohongshu.js +288 -0
- package/src/lib/youtube.js +1 -1
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
|
|
@@ -53,11 +54,11 @@ npm install puppeteer
|
|
|
53
54
|
```javascript
|
|
54
55
|
const { lmna, aioDownloader } = require("@lmna22/aio-downloader");
|
|
55
56
|
|
|
56
|
-
//
|
|
57
|
+
// Recommended: use the lmna namespace
|
|
57
58
|
const result = await lmna.youtube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 5);
|
|
58
59
|
console.log(result);
|
|
59
60
|
|
|
60
|
-
//
|
|
61
|
+
// Unified: auto-detect platform from URL
|
|
61
62
|
const result2 = await aioDownloader("https://www.instagram.com/p/ABC123/");
|
|
62
63
|
console.log(result2);
|
|
63
64
|
```
|
|
@@ -71,209 +72,53 @@ console.log(result2);
|
|
|
71
72
|
```javascript
|
|
72
73
|
const { lmna } = require("@lmna22/aio-downloader");
|
|
73
74
|
|
|
74
|
-
// Quality
|
|
75
|
-
// 1 = 144p, 2 = 360p, 3 = 480p, 4 = 720p,
|
|
76
|
-
// 5 = 1080p, 6 = 1440p, 7 = 2160p,
|
|
77
|
-
// 8 = Audio only (MP3), 9 = Get bitrate list
|
|
78
|
-
|
|
79
|
-
// Download a video in 1080p
|
|
75
|
+
// Quality: 1=144p, 2=360p, 5=1080p, 8=MP3, 9=bitrate list
|
|
80
76
|
const result = await lmna.youtube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 5);
|
|
81
77
|
|
|
82
78
|
if (result.status) {
|
|
83
|
-
console.log(result.data.title);
|
|
84
|
-
console.log(result.data.channel); // "Rick Astley"
|
|
85
|
-
console.log(result.data.views); // 1500000000
|
|
86
|
-
console.log(result.data.size); // Buffer size in bytes
|
|
87
|
-
console.log(result.data.type); // "mp4" or "mp3"
|
|
88
|
-
|
|
89
|
-
// Save to file
|
|
79
|
+
console.log(result.data.title);
|
|
90
80
|
const fs = require("fs");
|
|
91
81
|
fs.writeFileSync(`${result.data.title}.${result.data.type}`, result.data.result);
|
|
92
82
|
}
|
|
93
|
-
|
|
94
|
-
// Download audio only
|
|
95
|
-
const audio = await lmna.youtube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 8);
|
|
96
|
-
|
|
97
|
-
// Get available audio bitrates
|
|
98
|
-
const bitrates = await lmna.youtube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 9);
|
|
99
|
-
console.log(bitrates.data.bitrateList);
|
|
100
|
-
|
|
101
|
-
// Download entire playlist
|
|
102
|
-
const playlist = await lmna.youtubePlaylist(
|
|
103
|
-
"https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf",
|
|
104
|
-
5, // quality
|
|
105
|
-
"./my-playlist" // output folder
|
|
106
|
-
);
|
|
107
83
|
```
|
|
108
84
|
|
|
109
85
|
### Instagram
|
|
110
86
|
|
|
111
87
|
```javascript
|
|
112
88
|
const { lmna } = require("@lmna22/aio-downloader");
|
|
113
|
-
|
|
114
89
|
const result = await lmna.instagram("https://www.instagram.com/p/ABC123/");
|
|
115
|
-
|
|
116
|
-
if (result.status) {
|
|
117
|
-
console.log(result.data.url); // Array of download URLs
|
|
118
|
-
console.log(result.data.caption); // Post caption
|
|
119
|
-
console.log(result.data.username); // "@username"
|
|
120
|
-
console.log(result.data.like); // Like count
|
|
121
|
-
console.log(result.data.comment); // Comment count
|
|
122
|
-
console.log(result.data.isVideo); // true/false
|
|
123
|
-
|
|
124
|
-
// Download all media
|
|
125
|
-
const { download } = require("@lmna22/aio-downloader");
|
|
126
|
-
for (let i = 0; i < result.data.url.length; i++) {
|
|
127
|
-
await download(result.data.url[i], `./downloads/ig_${i + 1}.mp4`);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
90
|
```
|
|
131
91
|
|
|
132
92
|
### TikTok
|
|
133
93
|
|
|
134
94
|
```javascript
|
|
135
95
|
const { lmna } = require("@lmna22/aio-downloader");
|
|
136
|
-
|
|
137
96
|
const result = await lmna.tiktok("https://www.tiktok.com/@user/video/1234567890");
|
|
138
|
-
|
|
139
|
-
if (result.status) {
|
|
140
|
-
const data = result.data;
|
|
141
|
-
|
|
142
|
-
console.log(data.description); // Video description
|
|
143
|
-
console.log(data.author.nickname); // Author name
|
|
144
|
-
console.log(data.author.uniqueId); // @username
|
|
145
|
-
console.log(data.stats.likes); // Like count
|
|
146
|
-
console.log(data.stats.comments); // Comment count
|
|
147
|
-
console.log(data.stats.plays); // Play count
|
|
148
|
-
console.log(data.music.title); // Music title
|
|
149
|
-
console.log(data.videoInfo.duration); // Duration in seconds
|
|
150
|
-
console.log(data.videoInfo.width); // Video width
|
|
151
|
-
console.log(data.videoInfo.height); // Video height
|
|
152
|
-
|
|
153
|
-
// Video is returned as a Buffer — save directly
|
|
154
|
-
if (data.videoBuffer) {
|
|
155
|
-
const fs = require("fs");
|
|
156
|
-
fs.writeFileSync("tiktok_video.mp4", data.videoBuffer);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// For photo/slide posts
|
|
160
|
-
if (data.images) {
|
|
161
|
-
console.log(data.images); // Array of image URLs
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
97
|
```
|
|
165
98
|
|
|
166
|
-
###
|
|
99
|
+
### Xiaohongshu / RedNote
|
|
167
100
|
|
|
168
101
|
```javascript
|
|
169
102
|
const { lmna } = require("@lmna22/aio-downloader");
|
|
170
103
|
|
|
171
|
-
|
|
172
|
-
const data = await lmna.pinterest("https://www.pinterest.com/pin/123456789/");
|
|
173
|
-
|
|
174
|
-
// Or search by keyword
|
|
175
|
-
const searchResults = await lmna.pinterest("aesthetic wallpaper", { limit: 20 });
|
|
176
|
-
|
|
177
|
-
console.log(data.results);
|
|
178
|
-
// [
|
|
179
|
-
// {
|
|
180
|
-
// id: "123456789",
|
|
181
|
-
// title: "Beautiful Wallpaper",
|
|
182
|
-
// link: "https://www.pinterest.com/pin/123456789/",
|
|
183
|
-
// image: "https://i.pinimg.com/originals/...",
|
|
184
|
-
// source: "example.com"
|
|
185
|
-
// }
|
|
186
|
-
// ]
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### Pixiv
|
|
190
|
-
|
|
191
|
-
```javascript
|
|
192
|
-
const { lmna } = require("@lmna22/aio-downloader");
|
|
193
|
-
|
|
194
|
-
// From an artwork URL
|
|
195
|
-
const data = await lmna.pixiv("https://www.pixiv.net/artworks/12345678");
|
|
196
|
-
|
|
197
|
-
// Or search by tag/keyword
|
|
198
|
-
const searchResults = await lmna.pixiv("landscape", { limit: 5 });
|
|
199
|
-
|
|
200
|
-
console.log(data.results);
|
|
201
|
-
// [
|
|
202
|
-
// {
|
|
203
|
-
// id: "12345678",
|
|
204
|
-
// title: "Beautiful Landscape",
|
|
205
|
-
// link: "https://www.pixiv.net/artworks/12345678",
|
|
206
|
-
// image: "https://i.pximg.net/img-original/...",
|
|
207
|
-
// artist: "ArtistName",
|
|
208
|
-
// artistUrl: "https://www.pixiv.net/users/999",
|
|
209
|
-
// userId: "999",
|
|
210
|
-
// images: ["https://i.pximg.net/img-original/..."]
|
|
211
|
-
// }
|
|
212
|
-
// ]
|
|
213
|
-
|
|
214
|
-
// Skip enrichment for faster results (no original resolution images)
|
|
215
|
-
const fast = await lmna.pixiv("landscape", { limit: 10, enrich: false });
|
|
216
|
-
```
|
|
217
|
-
|
|
218
|
-
### X / Twitter
|
|
219
|
-
|
|
220
|
-
```javascript
|
|
221
|
-
const { lmna } = require("@lmna22/aio-downloader");
|
|
222
|
-
|
|
223
|
-
const result = await lmna.twitter("https://x.com/user/status/1234567890");
|
|
104
|
+
const result = await lmna.xiaohongshu("https://www.xiaohongshu.com/explore/abc123");
|
|
224
105
|
|
|
225
106
|
if (result.status) {
|
|
226
|
-
console.log(result.data.
|
|
227
|
-
console.log(result.data.
|
|
228
|
-
console.log(result.data.
|
|
229
|
-
console.log(result.data.
|
|
230
|
-
console.log(result.data.
|
|
231
|
-
console.log(result.data.
|
|
232
|
-
|
|
233
|
-
// result.data.result contains media items:
|
|
234
|
-
// [
|
|
235
|
-
// { type: "video", thumb: "https://...", url: "https://..." },
|
|
236
|
-
// { type: "image", url: "https://...?format=png&name=large" },
|
|
237
|
-
// { type: "gif", thumb: "https://...", url: "https://..." }
|
|
238
|
-
// ]
|
|
239
|
-
|
|
240
|
-
// Download all media
|
|
241
|
-
const { download } = require("@lmna22/aio-downloader");
|
|
242
|
-
for (let i = 0; i < result.data.result.length; i++) {
|
|
243
|
-
const media = result.data.result[i];
|
|
244
|
-
const ext = media.type === "image" ? ".png" : ".mp4";
|
|
245
|
-
await download(media.url, `./downloads/tweet_${i + 1}${ext}`);
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
```
|
|
249
|
-
|
|
250
|
-
### Lahelu
|
|
251
|
-
|
|
252
|
-
```javascript
|
|
253
|
-
const { lmna } = require("@lmna22/aio-downloader");
|
|
254
|
-
|
|
255
|
-
const result = await lmna.lahelu("https://lahelu.com/post/abc123");
|
|
256
|
-
|
|
257
|
-
if (result.status) {
|
|
258
|
-
const data = result.data;
|
|
259
|
-
|
|
260
|
-
console.log(data.title); // Post title
|
|
261
|
-
console.log(data.author); // Username
|
|
262
|
-
console.log(data.createdAt); // Creation date
|
|
263
|
-
console.log(data.stats.views); // View count
|
|
264
|
-
console.log(data.stats.likes); // Like count
|
|
265
|
-
console.log(data.stats.comments); // Comment count
|
|
266
|
-
console.log(data.media.type); // "video" or "image"
|
|
267
|
-
console.log(data.media.format); // File format (mp4, jpg, etc.)
|
|
268
|
-
console.log(data.media.url); // Direct download URL
|
|
269
|
-
console.log(data.fileName); // Suggested filename
|
|
270
|
-
|
|
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
|
+
|
|
271
114
|
// Download the media
|
|
272
115
|
const { download } = require("@lmna22/aio-downloader");
|
|
273
|
-
await download(data.media.url, `./downloads/${data.fileName}`);
|
|
116
|
+
await download(result.data.media.url, `./downloads/${result.data.fileName}`);
|
|
274
117
|
}
|
|
275
118
|
```
|
|
276
119
|
|
|
120
|
+
*(See the API Reference below for other platforms: Pinterest, Pixiv, Twitter, Lahelu)*
|
|
121
|
+
|
|
277
122
|
---
|
|
278
123
|
|
|
279
124
|
## 📥 Download Helper
|
|
@@ -309,6 +154,9 @@ await aioDownloader("https://www.pinterest.com/pin/456/");
|
|
|
309
154
|
await aioDownloader("https://www.pixiv.net/artworks/789");
|
|
310
155
|
await aioDownloader("https://x.com/user/status/101112");
|
|
311
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");
|
|
312
160
|
|
|
313
161
|
// Force a specific platform
|
|
314
162
|
await aioDownloader("https://example.com/video", { platform: "youtube", quality: 5 });
|
|
@@ -322,6 +170,9 @@ detectPlatform("https://pin.it/abc123"); // "pinterest"
|
|
|
322
170
|
detectPlatform("https://pixiv.net/artworks/456"); // "pixiv"
|
|
323
171
|
detectPlatform("https://x.com/user/status/789"); // "twitter"
|
|
324
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"
|
|
325
176
|
detectPlatform("https://unknown.com"); // null
|
|
326
177
|
```
|
|
327
178
|
|
|
@@ -329,7 +180,9 @@ detectPlatform("https://unknown.com"); // null
|
|
|
329
180
|
|
|
330
181
|
## 📚 API Reference
|
|
331
182
|
|
|
332
|
-
### `lmna` Namespace (
|
|
183
|
+
### `lmna` Namespace (Mandatory for individual scrapers)
|
|
184
|
+
|
|
185
|
+
Instead of individual function exports, all scrapers are now grouped under the `lmna` object:
|
|
333
186
|
|
|
334
187
|
```javascript
|
|
335
188
|
const { lmna } = require("@lmna22/aio-downloader");
|
|
@@ -338,10 +191,13 @@ await lmna.youtube(url, quality);
|
|
|
338
191
|
await lmna.youtubePlaylist(url, quality, folderPath);
|
|
339
192
|
await lmna.instagram(url);
|
|
340
193
|
await lmna.tiktok(url);
|
|
341
|
-
await lmna.pinterest(
|
|
342
|
-
await lmna.pixiv(
|
|
194
|
+
await lmna.pinterest(url, options);
|
|
195
|
+
await lmna.pixiv(url, options);
|
|
343
196
|
await lmna.twitter(url);
|
|
344
197
|
await lmna.lahelu(url);
|
|
198
|
+
await lmna.xiaohongshu(url);
|
|
199
|
+
|
|
200
|
+
// All return: { status, platform, data?, message? }
|
|
345
201
|
```
|
|
346
202
|
|
|
347
203
|
---
|
|
@@ -358,137 +214,29 @@ Auto-detect platform and scrape media data.
|
|
|
358
214
|
|
|
359
215
|
---
|
|
360
216
|
|
|
361
|
-
###
|
|
362
|
-
|
|
363
|
-
| Parameter | Type | Description |
|
|
364
|
-
|---|---|---|
|
|
365
|
-
| `url` | `string` | YouTube video URL |
|
|
366
|
-
| `quality` | `number` | 1=144p, 2=360p, 3=480p, 4=720p, 5=1080p, 6=1440p, 7=2160p, 8=MP3, 9=bitrate list |
|
|
367
|
-
|
|
368
|
-
**Returns:** `{ status, platform, data: { title, result (Buffer), size, quality, desc, views, likes, channel, uploadDate, thumb, type } }`
|
|
369
|
-
|
|
370
|
-
---
|
|
371
|
-
|
|
372
|
-
### `lmna.youtubePlaylist(url, quality, folderPath?)`
|
|
373
|
-
|
|
374
|
-
Downloads all videos from a YouTube playlist.
|
|
375
|
-
|
|
376
|
-
**Returns:** `{ status, platform, data: { title, resultPath[], metadata[] } }`
|
|
377
|
-
|
|
378
|
-
---
|
|
379
|
-
|
|
380
|
-
### `lmna.instagram(url)`
|
|
381
|
-
|
|
382
|
-
Uses 4 fallback methods for maximum reliability.
|
|
383
|
-
|
|
384
|
-
**Returns:** `{ status, platform, data: { url[], caption, username, like, comment, isVideo } }`
|
|
385
|
-
|
|
386
|
-
---
|
|
387
|
-
|
|
388
|
-
### `lmna.tiktok(url)`
|
|
389
|
-
|
|
390
|
-
Returns video as a Buffer (no watermark). Uses direct scraping with tikwm.com API fallback.
|
|
391
|
-
|
|
392
|
-
**Returns:** `{ status, platform, data: { videoId, description, videoUrl, videoBuffer (Buffer), videoInfo, author, music, stats, locationCreated, images? } }`
|
|
393
|
-
|
|
394
|
-
---
|
|
395
|
-
|
|
396
|
-
### `lmna.pinterest(input, options?)`
|
|
397
|
-
|
|
398
|
-
| Parameter | Type | Description |
|
|
399
|
-
|---|---|---|
|
|
400
|
-
| `input` | `string` | Pin URL or search keyword |
|
|
401
|
-
| `options.limit` | `number` | Max results for search (default: 10) |
|
|
402
|
-
|
|
403
|
-
**Returns:** `{ status, platform, method, total, results: [{ id, title, link, image, source }] }`
|
|
404
|
-
|
|
405
|
-
---
|
|
406
|
-
|
|
407
|
-
### `lmna.pixiv(input, options?)`
|
|
408
|
-
|
|
409
|
-
| Parameter | Type | Description |
|
|
410
|
-
|---|---|---|
|
|
411
|
-
| `input` | `string` | Artwork URL or search keyword |
|
|
412
|
-
| `options.limit` | `number` | Max results (default: 10) |
|
|
413
|
-
| `options.enrich` | `boolean` | Fetch original resolution images (default: true) |
|
|
414
|
-
|
|
415
|
-
**Returns:** `{ status, platform, method, total, results: [{ id, title, link, image, artist, artistUrl, userId, images[] }] }`
|
|
416
|
-
|
|
417
|
-
---
|
|
418
|
-
|
|
419
|
-
### `lmna.twitter(url)`
|
|
420
|
-
|
|
421
|
-
Extracts best quality video/image/gif from tweets via Twitter GraphQL API.
|
|
422
|
-
|
|
423
|
-
**Returns:** `{ status, platform, data: { author, like, view, retweet, description, sensitiveContent, result: [{ type, url, thumb? }] } }`
|
|
424
|
-
|
|
425
|
-
---
|
|
426
|
-
|
|
427
|
-
### `lmna.lahelu(url)`
|
|
428
|
-
|
|
429
|
-
Scrapes video or image from Lahelu posts.
|
|
430
|
-
|
|
431
|
-
**Returns:** `{ status, platform, data: { postId, title, author, createdAt, stats: { views, likes, comments }, media: { type, format, url, desc }, fileName } }`
|
|
432
|
-
|
|
433
|
-
---
|
|
434
|
-
|
|
435
|
-
### `download(url, outputPath, options?)`
|
|
436
|
-
|
|
437
|
-
Helper to download any file to disk.
|
|
438
|
-
|
|
439
|
-
| Parameter | Type | Description |
|
|
440
|
-
|---|---|---|
|
|
441
|
-
| `url` | `string` | Direct download URL |
|
|
442
|
-
| `outputPath` | `string` | Local file path |
|
|
443
|
-
| `options.headers` | `object` | Custom request headers |
|
|
444
|
-
| `options.timeout` | `number` | Timeout in ms (default: 120000) |
|
|
445
|
-
| `options.onProgress` | `function` | `({ downloaded, total, percentage })` |
|
|
446
|
-
|
|
447
|
-
**Returns:** `{ path, size, filename }`
|
|
448
|
-
|
|
449
|
-
---
|
|
450
|
-
|
|
451
|
-
### `detectPlatform(url)`
|
|
452
|
-
|
|
453
|
-
**Returns:** `"youtube"` | `"instagram"` | `"tiktok"` | `"pinterest"` | `"pixiv"` | `"twitter"` | `"lahelu"` | `null`
|
|
454
|
-
|
|
455
|
-
---
|
|
456
|
-
|
|
457
|
-
## 🔄 Migration from v1.0.x
|
|
458
|
-
|
|
459
|
-
If you're upgrading from the old API, the legacy named exports still work:
|
|
217
|
+
### Utility Exports
|
|
460
218
|
|
|
461
219
|
```javascript
|
|
462
|
-
|
|
463
|
-
const { tiktokDownloader } = require("@lmna22/aio-downloader");
|
|
464
|
-
await tiktokDownloader(url);
|
|
465
|
-
|
|
466
|
-
// New way (recommended)
|
|
467
|
-
const { lmna } = require("@lmna22/aio-downloader");
|
|
468
|
-
await lmna.tiktok(url);
|
|
220
|
+
const { detectPlatform, download } = require("@lmna22/aio-downloader");
|
|
469
221
|
```
|
|
470
222
|
|
|
223
|
+
| Function | Description |
|
|
224
|
+
|---|---|
|
|
225
|
+
| `detectPlatform(url)` | Returns the platform name from URL |
|
|
226
|
+
| `download(url, path, options)` | Helper to download files to disk |
|
|
227
|
+
|
|
471
228
|
---
|
|
472
229
|
|
|
473
230
|
## ⚠️ Error Handling
|
|
474
231
|
|
|
475
|
-
All functions return `{ status: false, platform: "...", message: "..." }` on failure
|
|
476
|
-
|
|
477
|
-
```javascript
|
|
478
|
-
const { lmna } = require("@lmna22/aio-downloader");
|
|
479
|
-
|
|
480
|
-
const result = await lmna.youtube("https://www.youtube.com/watch?v=invalid", 5);
|
|
481
|
-
if (!result.status) {
|
|
482
|
-
console.error("Failed:", result.message);
|
|
483
|
-
}
|
|
484
|
-
```
|
|
232
|
+
All functions return `{ status: false, platform: "...", message: "..." }` on failure.
|
|
485
233
|
|
|
486
234
|
---
|
|
487
235
|
|
|
488
236
|
## 📋 Requirements
|
|
489
237
|
|
|
490
238
|
- **Node.js** >= 14.0.0
|
|
491
|
-
- **puppeteer** (optional) — fallback for Pinterest/Pixiv
|
|
239
|
+
- **puppeteer** (optional) — fallback for Pinterest/Pixiv
|
|
492
240
|
|
|
493
241
|
---
|
|
494
242
|
|
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,17 +37,10 @@
|
|
|
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
|
-
"youtube-dl-exec": "^3.
|
|
43
|
-
},
|
|
44
|
-
"peerDependencies": {
|
|
45
|
-
"puppeteer": ">=20.0.0"
|
|
46
|
-
},
|
|
47
|
-
"peerDependenciesMeta": {
|
|
48
|
-
"puppeteer": {
|
|
49
|
-
"optional": true
|
|
50
|
-
}
|
|
43
|
+
"youtube-dl-exec": "^3.1.4"
|
|
51
44
|
},
|
|
52
45
|
"engines": {
|
|
53
46
|
"node": ">=14.0.0"
|
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 = {
|
|
@@ -67,13 +71,4 @@ module.exports = {
|
|
|
67
71
|
aioDownloader,
|
|
68
72
|
detectPlatform,
|
|
69
73
|
download,
|
|
70
|
-
// Legacy named exports (backward compatible)
|
|
71
|
-
youtubeDownloader,
|
|
72
|
-
youtubePlaylistDownloader,
|
|
73
|
-
instagramDownloader,
|
|
74
|
-
tiktokDownloader,
|
|
75
|
-
pinterestDownloader,
|
|
76
|
-
pixivDownloader,
|
|
77
|
-
twitterDownloader,
|
|
78
|
-
laheluDownloader,
|
|
79
74
|
};
|
package/src/lib/lahelu.js
CHANGED
|
@@ -25,22 +25,18 @@ function extractPostId(url) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
async function fetchLaheluData(postId) {
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const post = res.data.postInfos.find(p => p.postId === postId || p.postID === postId);
|
|
41
|
-
if (post) return post;
|
|
42
|
-
if (!res.data.hasMore) break;
|
|
43
|
-
}
|
|
28
|
+
const apiUrl = `https://lahelu.com/api/post/get?postID=${postId}`;
|
|
29
|
+
|
|
30
|
+
const res = await axios.get(apiUrl, {
|
|
31
|
+
headers: {
|
|
32
|
+
"User-Agent": DEFAULT_UA,
|
|
33
|
+
"Referer": "https://lahelu.com/",
|
|
34
|
+
},
|
|
35
|
+
timeout: 15000,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (res.data && res.data.postInfo) {
|
|
39
|
+
return res.data.postInfo;
|
|
44
40
|
}
|
|
45
41
|
return null;
|
|
46
42
|
}
|
|
@@ -49,36 +45,35 @@ function normalizeMedia(post) {
|
|
|
49
45
|
const CACHE_URL = "https://cache.lahelu.com/";
|
|
50
46
|
const medias = [];
|
|
51
47
|
|
|
52
|
-
if (post.
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
url: videoUrl,
|
|
58
|
-
desc: "Video",
|
|
59
|
-
});
|
|
60
|
-
} else if (post.mediaType === "image" || post.type === "image") {
|
|
61
|
-
const imageUrl = post.media?.startsWith("http") ? post.media : CACHE_URL + post.media;
|
|
48
|
+
if (post.media) {
|
|
49
|
+
const mediaUrl = post.media.startsWith("http") ? post.media : CACHE_URL + post.media;
|
|
50
|
+
const isVideo = mediaUrl.match(/\.(mp4|webm|mov)(\?.*)?$/i) || post.mediaType === 1 || post.type === 1;
|
|
51
|
+
const format = isVideo ? "mp4" : getExtFromUrl(mediaUrl, ".jpg");
|
|
52
|
+
|
|
62
53
|
medias.push({
|
|
63
|
-
type: "image",
|
|
64
|
-
format:
|
|
65
|
-
url:
|
|
66
|
-
desc: "Image",
|
|
54
|
+
type: isVideo ? "video" : "image",
|
|
55
|
+
format: format,
|
|
56
|
+
url: mediaUrl,
|
|
57
|
+
desc: isVideo ? "Video" : "Image",
|
|
67
58
|
});
|
|
68
59
|
}
|
|
69
60
|
|
|
70
|
-
if (post.content && Array.isArray(post.content)
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const isVideo = firstItem.type === "video" || (firstItem.value && firstItem.value.match(/\.(mp4|webm|mov)(\?.*)?$/i));
|
|
74
|
-
const mediaType = isVideo ? "video" : "image";
|
|
75
|
-
const format = mediaType === "video" ? "mp4" : getExtFromUrl(mediaUrl, ".jpg");
|
|
61
|
+
if (post.content && Array.isArray(post.content)) {
|
|
62
|
+
post.content.forEach((item) => {
|
|
63
|
+
if (!item.value) return;
|
|
76
64
|
|
|
77
|
-
|
|
78
|
-
type
|
|
79
|
-
format:
|
|
80
|
-
|
|
81
|
-
|
|
65
|
+
const mediaUrl = item.value.startsWith("http") ? item.value : CACHE_URL + item.value;
|
|
66
|
+
const isVideo = mediaUrl.match(/\.(mp4|webm|mov)(\?.*)?$/i) || item.type === 1 || item.type === 4;
|
|
67
|
+
const format = isVideo ? "mp4" : getExtFromUrl(mediaUrl, ".jpg");
|
|
68
|
+
|
|
69
|
+
if (medias.some(m => m.url === mediaUrl)) return;
|
|
70
|
+
|
|
71
|
+
medias.push({
|
|
72
|
+
type: isVideo ? "video" : "image",
|
|
73
|
+
format: format,
|
|
74
|
+
url: mediaUrl,
|
|
75
|
+
desc: isVideo ? "Video" : "Image",
|
|
76
|
+
});
|
|
82
77
|
});
|
|
83
78
|
}
|
|
84
79
|
|
|
@@ -126,7 +121,7 @@ async function laheluDownloader(url) {
|
|
|
126
121
|
|
|
127
122
|
const title = post.title || "Untitled";
|
|
128
123
|
const author = post.userUsername || post.userInfo?.username || "Unknown";
|
|
129
|
-
const createdAt = post.createTime ? new Date(post.createTime
|
|
124
|
+
const createdAt = post.createTime ? new Date(post.createTime).toLocaleDateString("en-US") : "-";
|
|
130
125
|
const views = parseInt(post.totalViews || 0, 10);
|
|
131
126
|
const likes = parseInt(post.totalUpvotes || 0, 10);
|
|
132
127
|
const comments = parseInt(post.totalComments || 0, 10);
|
|
@@ -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;
|
package/src/lib/youtube.js
CHANGED
|
@@ -34,7 +34,7 @@ async function youtubeDownloader(link, qualityIndex) {
|
|
|
34
34
|
return {
|
|
35
35
|
status: false,
|
|
36
36
|
platform: "youtube",
|
|
37
|
-
message:
|
|
37
|
+
message: "youtube-dl-exec is not installed. Install it with: npm install youtube-dl-exec",
|
|
38
38
|
};
|
|
39
39
|
}
|
|
40
40
|
try {
|