@lmna22/aio-downloader 1.0.3 → 2.0.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 +122 -41
- package/package.json +2 -2
- package/src/index.js +18 -1
- package/src/lib/lahelu.js +170 -0
- package/src/lib/tiktok.js +199 -79
- package/src/lib/twitter.js +22 -5
- package/src/lib/youtube.js +1 -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,
|
|
3
|
+
> All-in-one media downloader for YouTube, Instagram, TikTok, Pinterest, Pixiv, X/Twitter, and Lahelu.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@lmna22/aio-downloader)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -16,17 +16,18 @@ Scrape and download videos, audio, and images from multiple platforms with a sin
|
|
|
16
16
|
|---|---|---|---|---|
|
|
17
17
|
| **YouTube** | ✅ (Multiple qualities) | ✅ (MP3) | — | — |
|
|
18
18
|
| **Instagram** | ✅ | — | ✅ | — |
|
|
19
|
-
| **TikTok** | ✅ (Buffer) | ✅ |
|
|
19
|
+
| **TikTok** | ✅ (Buffer) | ✅ | ✅ (Photo/Slides) | — |
|
|
20
20
|
| **Pinterest** | — | — | ✅ | ✅ |
|
|
21
21
|
| **Pixiv** | — | — | ✅ (Original Resolution) | ✅ |
|
|
22
22
|
| **X / Twitter** | ✅ (Best quality) | — | ✅ | — |
|
|
23
|
+
| **Lahelu** | ✅ | — | ✅ | — |
|
|
23
24
|
|
|
24
25
|
- 🔗 **Auto-detect platform** from URL — just pass any supported link
|
|
25
26
|
- 📦 **Programmatic API** — designed for Node.js applications, bots, and scripts
|
|
26
27
|
- 📥 **Built-in download helper** with progress callback
|
|
27
28
|
- 🔍 **Search support** for Pinterest and Pixiv (pass keywords instead of URLs)
|
|
28
29
|
- 🚫 **No API keys** — all data is scraped from public sources
|
|
29
|
-
- 🔄 **Multi-method fallback** — Instagram uses 4
|
|
30
|
+
- 🔄 **Multi-method fallback** — Instagram uses 4 methods, TikTok uses 2 methods for maximum reliability
|
|
30
31
|
- 🎬 **YouTube quality selection** — choose from 144p to 2160p, or audio-only MP3
|
|
31
32
|
|
|
32
33
|
---
|
|
@@ -50,11 +51,15 @@ npm install puppeteer
|
|
|
50
51
|
## 🚀 Quick Start
|
|
51
52
|
|
|
52
53
|
```javascript
|
|
53
|
-
const { aioDownloader } = require("@lmna22/aio-downloader");
|
|
54
|
+
const { lmna, aioDownloader } = require("@lmna22/aio-downloader");
|
|
54
55
|
|
|
55
|
-
//
|
|
56
|
-
const result = await
|
|
56
|
+
// Using the lmna namespace (recommended)
|
|
57
|
+
const result = await lmna.youtube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 5);
|
|
57
58
|
console.log(result);
|
|
59
|
+
|
|
60
|
+
// Or auto-detect platform from URL
|
|
61
|
+
const result2 = await aioDownloader("https://www.instagram.com/p/ABC123/");
|
|
62
|
+
console.log(result2);
|
|
58
63
|
```
|
|
59
64
|
|
|
60
65
|
---
|
|
@@ -64,7 +69,7 @@ console.log(result);
|
|
|
64
69
|
### YouTube
|
|
65
70
|
|
|
66
71
|
```javascript
|
|
67
|
-
const {
|
|
72
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
68
73
|
|
|
69
74
|
// Quality options:
|
|
70
75
|
// 1 = 144p, 2 = 360p, 3 = 480p, 4 = 720p,
|
|
@@ -72,7 +77,7 @@ const { youtubeDownloader, youtubePlaylistDownloader } = require("@lmna22/aio-do
|
|
|
72
77
|
// 8 = Audio only (MP3), 9 = Get bitrate list
|
|
73
78
|
|
|
74
79
|
// Download a video in 1080p
|
|
75
|
-
const result = await
|
|
80
|
+
const result = await lmna.youtube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 5);
|
|
76
81
|
|
|
77
82
|
if (result.status) {
|
|
78
83
|
console.log(result.data.title); // "Rick Astley - Never Gonna Give You Up"
|
|
@@ -87,14 +92,14 @@ if (result.status) {
|
|
|
87
92
|
}
|
|
88
93
|
|
|
89
94
|
// Download audio only
|
|
90
|
-
const audio = await
|
|
95
|
+
const audio = await lmna.youtube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 8);
|
|
91
96
|
|
|
92
97
|
// Get available audio bitrates
|
|
93
|
-
const bitrates = await
|
|
98
|
+
const bitrates = await lmna.youtube("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 9);
|
|
94
99
|
console.log(bitrates.data.bitrateList);
|
|
95
100
|
|
|
96
101
|
// Download entire playlist
|
|
97
|
-
const playlist = await
|
|
102
|
+
const playlist = await lmna.youtubePlaylist(
|
|
98
103
|
"https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf",
|
|
99
104
|
5, // quality
|
|
100
105
|
"./my-playlist" // output folder
|
|
@@ -104,9 +109,9 @@ const playlist = await youtubePlaylistDownloader(
|
|
|
104
109
|
### Instagram
|
|
105
110
|
|
|
106
111
|
```javascript
|
|
107
|
-
const {
|
|
112
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
108
113
|
|
|
109
|
-
const result = await
|
|
114
|
+
const result = await lmna.instagram("https://www.instagram.com/p/ABC123/");
|
|
110
115
|
|
|
111
116
|
if (result.status) {
|
|
112
117
|
console.log(result.data.url); // Array of download URLs
|
|
@@ -127,9 +132,9 @@ if (result.status) {
|
|
|
127
132
|
### TikTok
|
|
128
133
|
|
|
129
134
|
```javascript
|
|
130
|
-
const {
|
|
135
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
131
136
|
|
|
132
|
-
const result = await
|
|
137
|
+
const result = await lmna.tiktok("https://www.tiktok.com/@user/video/1234567890");
|
|
133
138
|
|
|
134
139
|
if (result.status) {
|
|
135
140
|
const data = result.data;
|
|
@@ -150,19 +155,24 @@ if (result.status) {
|
|
|
150
155
|
const fs = require("fs");
|
|
151
156
|
fs.writeFileSync("tiktok_video.mp4", data.videoBuffer);
|
|
152
157
|
}
|
|
158
|
+
|
|
159
|
+
// For photo/slide posts
|
|
160
|
+
if (data.images) {
|
|
161
|
+
console.log(data.images); // Array of image URLs
|
|
162
|
+
}
|
|
153
163
|
}
|
|
154
164
|
```
|
|
155
165
|
|
|
156
166
|
### Pinterest
|
|
157
167
|
|
|
158
168
|
```javascript
|
|
159
|
-
const {
|
|
169
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
160
170
|
|
|
161
171
|
// From a direct pin URL
|
|
162
|
-
const data = await
|
|
172
|
+
const data = await lmna.pinterest("https://www.pinterest.com/pin/123456789/");
|
|
163
173
|
|
|
164
174
|
// Or search by keyword
|
|
165
|
-
const searchResults = await
|
|
175
|
+
const searchResults = await lmna.pinterest("aesthetic wallpaper", { limit: 20 });
|
|
166
176
|
|
|
167
177
|
console.log(data.results);
|
|
168
178
|
// [
|
|
@@ -179,13 +189,13 @@ console.log(data.results);
|
|
|
179
189
|
### Pixiv
|
|
180
190
|
|
|
181
191
|
```javascript
|
|
182
|
-
const {
|
|
192
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
183
193
|
|
|
184
194
|
// From an artwork URL
|
|
185
|
-
const data = await
|
|
195
|
+
const data = await lmna.pixiv("https://www.pixiv.net/artworks/12345678");
|
|
186
196
|
|
|
187
197
|
// Or search by tag/keyword
|
|
188
|
-
const searchResults = await
|
|
198
|
+
const searchResults = await lmna.pixiv("landscape", { limit: 5 });
|
|
189
199
|
|
|
190
200
|
console.log(data.results);
|
|
191
201
|
// [
|
|
@@ -202,15 +212,15 @@ console.log(data.results);
|
|
|
202
212
|
// ]
|
|
203
213
|
|
|
204
214
|
// Skip enrichment for faster results (no original resolution images)
|
|
205
|
-
const fast = await
|
|
215
|
+
const fast = await lmna.pixiv("landscape", { limit: 10, enrich: false });
|
|
206
216
|
```
|
|
207
217
|
|
|
208
218
|
### X / Twitter
|
|
209
219
|
|
|
210
220
|
```javascript
|
|
211
|
-
const {
|
|
221
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
212
222
|
|
|
213
|
-
const result = await
|
|
223
|
+
const result = await lmna.twitter("https://x.com/user/status/1234567890");
|
|
214
224
|
|
|
215
225
|
if (result.status) {
|
|
216
226
|
console.log(result.data.author); // "username"
|
|
@@ -237,6 +247,33 @@ if (result.status) {
|
|
|
237
247
|
}
|
|
238
248
|
```
|
|
239
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
|
+
|
|
271
|
+
// Download the media
|
|
272
|
+
const { download } = require("@lmna22/aio-downloader");
|
|
273
|
+
await download(data.media.url, `./downloads/${data.fileName}`);
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
240
277
|
---
|
|
241
278
|
|
|
242
279
|
## 📥 Download Helper
|
|
@@ -271,6 +308,7 @@ await aioDownloader("https://www.tiktok.com/@user/video/123");
|
|
|
271
308
|
await aioDownloader("https://www.pinterest.com/pin/456/");
|
|
272
309
|
await aioDownloader("https://www.pixiv.net/artworks/789");
|
|
273
310
|
await aioDownloader("https://x.com/user/status/101112");
|
|
311
|
+
await aioDownloader("https://lahelu.com/post/abc123");
|
|
274
312
|
|
|
275
313
|
// Force a specific platform
|
|
276
314
|
await aioDownloader("https://example.com/video", { platform: "youtube", quality: 5 });
|
|
@@ -280,8 +318,10 @@ detectPlatform("https://youtube.com/watch?v=abc"); // "youtube"
|
|
|
280
318
|
detectPlatform("https://instagram.com/p/xyz"); // "instagram"
|
|
281
319
|
detectPlatform("https://tiktok.com/@user/video/1"); // "tiktok"
|
|
282
320
|
detectPlatform("https://pinterest.com/pin/123"); // "pinterest"
|
|
321
|
+
detectPlatform("https://pin.it/abc123"); // "pinterest"
|
|
283
322
|
detectPlatform("https://pixiv.net/artworks/456"); // "pixiv"
|
|
284
323
|
detectPlatform("https://x.com/user/status/789"); // "twitter"
|
|
324
|
+
detectPlatform("https://lahelu.com/post/abc"); // "lahelu"
|
|
285
325
|
detectPlatform("https://unknown.com"); // null
|
|
286
326
|
```
|
|
287
327
|
|
|
@@ -289,6 +329,23 @@ detectPlatform("https://unknown.com"); // null
|
|
|
289
329
|
|
|
290
330
|
## 📚 API Reference
|
|
291
331
|
|
|
332
|
+
### `lmna` Namespace (Recommended)
|
|
333
|
+
|
|
334
|
+
```javascript
|
|
335
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
336
|
+
|
|
337
|
+
await lmna.youtube(url, quality);
|
|
338
|
+
await lmna.youtubePlaylist(url, quality, folderPath);
|
|
339
|
+
await lmna.instagram(url);
|
|
340
|
+
await lmna.tiktok(url);
|
|
341
|
+
await lmna.pinterest(input, options);
|
|
342
|
+
await lmna.pixiv(input, options);
|
|
343
|
+
await lmna.twitter(url);
|
|
344
|
+
await lmna.lahelu(url);
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
|
|
292
349
|
### `aioDownloader(url, options?)`
|
|
293
350
|
|
|
294
351
|
Auto-detect platform and scrape media data.
|
|
@@ -301,42 +358,42 @@ Auto-detect platform and scrape media data.
|
|
|
301
358
|
|
|
302
359
|
---
|
|
303
360
|
|
|
304
|
-
### `
|
|
361
|
+
### `lmna.youtube(url, quality)`
|
|
305
362
|
|
|
306
363
|
| Parameter | Type | Description |
|
|
307
364
|
|---|---|---|
|
|
308
365
|
| `url` | `string` | YouTube video URL |
|
|
309
366
|
| `quality` | `number` | 1=144p, 2=360p, 3=480p, 4=720p, 5=1080p, 6=1440p, 7=2160p, 8=MP3, 9=bitrate list |
|
|
310
367
|
|
|
311
|
-
**Returns:** `{
|
|
368
|
+
**Returns:** `{ status, platform, data: { title, result (Buffer), size, quality, desc, views, likes, channel, uploadDate, thumb, type } }`
|
|
312
369
|
|
|
313
370
|
---
|
|
314
371
|
|
|
315
|
-
### `
|
|
372
|
+
### `lmna.youtubePlaylist(url, quality, folderPath?)`
|
|
316
373
|
|
|
317
374
|
Downloads all videos from a YouTube playlist.
|
|
318
375
|
|
|
319
|
-
**Returns:** `{
|
|
376
|
+
**Returns:** `{ status, platform, data: { title, resultPath[], metadata[] } }`
|
|
320
377
|
|
|
321
378
|
---
|
|
322
379
|
|
|
323
|
-
### `
|
|
380
|
+
### `lmna.instagram(url)`
|
|
324
381
|
|
|
325
382
|
Uses 4 fallback methods for maximum reliability.
|
|
326
383
|
|
|
327
|
-
**Returns:** `{
|
|
384
|
+
**Returns:** `{ status, platform, data: { url[], caption, username, like, comment, isVideo } }`
|
|
328
385
|
|
|
329
386
|
---
|
|
330
387
|
|
|
331
|
-
### `
|
|
388
|
+
### `lmna.tiktok(url)`
|
|
332
389
|
|
|
333
|
-
Returns video as a Buffer (no watermark).
|
|
390
|
+
Returns video as a Buffer (no watermark). Uses direct scraping with tikwm.com API fallback.
|
|
334
391
|
|
|
335
|
-
**Returns:** `{
|
|
392
|
+
**Returns:** `{ status, platform, data: { videoId, description, videoUrl, videoBuffer (Buffer), videoInfo, author, music, stats, locationCreated, images? } }`
|
|
336
393
|
|
|
337
394
|
---
|
|
338
395
|
|
|
339
|
-
### `
|
|
396
|
+
### `lmna.pinterest(input, options?)`
|
|
340
397
|
|
|
341
398
|
| Parameter | Type | Description |
|
|
342
399
|
|---|---|---|
|
|
@@ -347,7 +404,7 @@ Returns video as a Buffer (no watermark).
|
|
|
347
404
|
|
|
348
405
|
---
|
|
349
406
|
|
|
350
|
-
### `
|
|
407
|
+
### `lmna.pixiv(input, options?)`
|
|
351
408
|
|
|
352
409
|
| Parameter | Type | Description |
|
|
353
410
|
|---|---|---|
|
|
@@ -359,11 +416,19 @@ Returns video as a Buffer (no watermark).
|
|
|
359
416
|
|
|
360
417
|
---
|
|
361
418
|
|
|
362
|
-
### `
|
|
419
|
+
### `lmna.twitter(url)`
|
|
363
420
|
|
|
364
421
|
Extracts best quality video/image/gif from tweets via Twitter GraphQL API.
|
|
365
422
|
|
|
366
|
-
**Returns:** `{
|
|
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 } }`
|
|
367
432
|
|
|
368
433
|
---
|
|
369
434
|
|
|
@@ -385,18 +450,34 @@ Helper to download any file to disk.
|
|
|
385
450
|
|
|
386
451
|
### `detectPlatform(url)`
|
|
387
452
|
|
|
388
|
-
**Returns:** `"youtube"` | `"instagram"` | `"tiktok"` | `"pinterest"` | `"pixiv"` | `"twitter"` | `null`
|
|
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:
|
|
460
|
+
|
|
461
|
+
```javascript
|
|
462
|
+
// Old way (still works)
|
|
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);
|
|
469
|
+
```
|
|
389
470
|
|
|
390
471
|
---
|
|
391
472
|
|
|
392
473
|
## ⚠️ Error Handling
|
|
393
474
|
|
|
394
|
-
All functions return `{ status: false, message: "..." }` on failure:
|
|
475
|
+
All functions return `{ status: false, platform: "...", message: "..." }` on failure:
|
|
395
476
|
|
|
396
477
|
```javascript
|
|
397
|
-
const {
|
|
478
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
398
479
|
|
|
399
|
-
const result = await
|
|
480
|
+
const result = await lmna.youtube("https://www.youtube.com/watch?v=invalid", 5);
|
|
400
481
|
if (!result.status) {
|
|
401
482
|
console.error("Failed:", result.message);
|
|
402
483
|
}
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmna22/aio-downloader",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0",
|
|
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": {
|
|
7
|
-
"test": "node -e \"const
|
|
7
|
+
"test": "node -e \"const { lmna, detectPlatform } = require('./src/index.js'); console.log('lmna methods:', Object.keys(lmna)); console.log('Platform detection:', detectPlatform('https://youtube.com/watch?v=test'));\""
|
|
8
8
|
},
|
|
9
9
|
"keywords": [
|
|
10
10
|
"downloader",
|
package/src/index.js
CHANGED
|
@@ -4,15 +4,17 @@ const tiktokDownloader = require("./lib/tiktok");
|
|
|
4
4
|
const pinterestDownloader = require("./lib/pinterest");
|
|
5
5
|
const pixivDownloader = require("./lib/pixiv");
|
|
6
6
|
const twitterDownloader = require("./lib/twitter");
|
|
7
|
+
const laheluDownloader = require("./lib/lahelu");
|
|
7
8
|
const download = require("./download");
|
|
8
9
|
|
|
9
10
|
const PLATFORM_PATTERNS = [
|
|
10
11
|
{ name: "youtube", test: (url) => /(?:youtube\.com\/watch|youtu\.be\/|youtube\.com\/shorts|youtube\.com\/playlist)/i.test(url) },
|
|
11
12
|
{ name: "instagram", test: (url) => /instagram\.com\//i.test(url) },
|
|
12
13
|
{ name: "tiktok", test: (url) => /tiktok\.com\//i.test(url) },
|
|
13
|
-
{ name: "pinterest", test: (url) => /pinterest
|
|
14
|
+
{ name: "pinterest", test: (url) => /(?:pinterest\.|pin\.it\/)/i.test(url) },
|
|
14
15
|
{ name: "pixiv", test: (url) => /pixiv\.net\//i.test(url) },
|
|
15
16
|
{ name: "twitter", test: (url) => /(?:twitter\.com\/|x\.com\/)/i.test(url) },
|
|
17
|
+
{ name: "lahelu", test: (url) => /lahelu\.com\/post\//i.test(url) },
|
|
16
18
|
];
|
|
17
19
|
|
|
18
20
|
function detectPlatform(url) {
|
|
@@ -29,6 +31,7 @@ const scrapers = {
|
|
|
29
31
|
pinterest: (url, options) => pinterestDownloader(url, options),
|
|
30
32
|
pixiv: (url, options) => pixivDownloader(url, options),
|
|
31
33
|
twitter: (url) => twitterDownloader(url),
|
|
34
|
+
lahelu: (url) => laheluDownloader(url),
|
|
32
35
|
};
|
|
33
36
|
|
|
34
37
|
async function aioDownloader(url, options = {}) {
|
|
@@ -48,10 +51,23 @@ async function aioDownloader(url, options = {}) {
|
|
|
48
51
|
return scraper(url, options);
|
|
49
52
|
}
|
|
50
53
|
|
|
54
|
+
const lmna = {
|
|
55
|
+
youtube: (url, quality) => youtubeDownloader(url, quality),
|
|
56
|
+
youtubePlaylist: (url, quality, folder) => youtubePlaylistDownloader(url, quality, folder),
|
|
57
|
+
instagram: (url) => instagramDownloader(url),
|
|
58
|
+
tiktok: (url) => tiktokDownloader(url),
|
|
59
|
+
pinterest: (url, options) => pinterestDownloader(url, options),
|
|
60
|
+
pixiv: (url, options) => pixivDownloader(url, options),
|
|
61
|
+
twitter: (url) => twitterDownloader(url),
|
|
62
|
+
lahelu: (url) => laheluDownloader(url),
|
|
63
|
+
};
|
|
64
|
+
|
|
51
65
|
module.exports = {
|
|
66
|
+
lmna,
|
|
52
67
|
aioDownloader,
|
|
53
68
|
detectPlatform,
|
|
54
69
|
download,
|
|
70
|
+
// Legacy named exports (backward compatible)
|
|
55
71
|
youtubeDownloader,
|
|
56
72
|
youtubePlaylistDownloader,
|
|
57
73
|
instagramDownloader,
|
|
@@ -59,4 +75,5 @@ module.exports = {
|
|
|
59
75
|
pinterestDownloader,
|
|
60
76
|
pixivDownloader,
|
|
61
77
|
twitterDownloader,
|
|
78
|
+
laheluDownloader,
|
|
62
79
|
};
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
const { DEFAULT_UA, sanitizeFileName, getExtFromUrl, formatNumber } = require("../utils");
|
|
3
|
+
|
|
4
|
+
function isLaheluUrl(url) {
|
|
5
|
+
try {
|
|
6
|
+
const u = new URL(url);
|
|
7
|
+
return u.hostname.includes("lahelu.com") && u.pathname.includes("/post/");
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function extractPostId(url) {
|
|
14
|
+
try {
|
|
15
|
+
const urlObj = new URL(url);
|
|
16
|
+
const paths = urlObj.pathname.split("/").filter(p => p);
|
|
17
|
+
if (paths.length >= 2 && paths[0] === "post" && paths[1]) {
|
|
18
|
+
return paths[1];
|
|
19
|
+
}
|
|
20
|
+
} catch {
|
|
21
|
+
const match = url.match(/lahelu\.com\/post\/([a-zA-Z0-9_-]+)/);
|
|
22
|
+
if (match) return match[1];
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function fetchLaheluData(postId) {
|
|
28
|
+
for (let page = 1; page <= 10; page++) {
|
|
29
|
+
const apiUrl = `https://lahelu.com/api/post/get-posts?feed=1&page=${page}`;
|
|
30
|
+
|
|
31
|
+
const res = await axios.get(apiUrl, {
|
|
32
|
+
headers: {
|
|
33
|
+
"User-Agent": DEFAULT_UA,
|
|
34
|
+
"Referer": "https://lahelu.com/",
|
|
35
|
+
},
|
|
36
|
+
timeout: 15000,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (res.data && res.data.postInfos && Array.isArray(res.data.postInfos)) {
|
|
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
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function normalizeMedia(post) {
|
|
49
|
+
const CACHE_URL = "https://cache.lahelu.com/";
|
|
50
|
+
const medias = [];
|
|
51
|
+
|
|
52
|
+
if (post.mediaType === "video" || post.type === "video") {
|
|
53
|
+
const videoUrl = post.media?.startsWith("http") ? post.media : CACHE_URL + post.media;
|
|
54
|
+
medias.push({
|
|
55
|
+
type: "video",
|
|
56
|
+
format: "mp4",
|
|
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;
|
|
62
|
+
medias.push({
|
|
63
|
+
type: "image",
|
|
64
|
+
format: getExtFromUrl(imageUrl, ".jpg"),
|
|
65
|
+
url: imageUrl,
|
|
66
|
+
desc: "Image",
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (post.content && Array.isArray(post.content) && post.content.length > 0) {
|
|
71
|
+
const firstItem = post.content[0];
|
|
72
|
+
const mediaUrl = firstItem.value?.startsWith("http") ? firstItem.value : CACHE_URL + firstItem.value;
|
|
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");
|
|
76
|
+
|
|
77
|
+
medias.push({
|
|
78
|
+
type: mediaType,
|
|
79
|
+
format: format,
|
|
80
|
+
url: mediaUrl,
|
|
81
|
+
desc: mediaType === "video" ? "Video" : "Image",
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return medias;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function laheluDownloader(url) {
|
|
89
|
+
try {
|
|
90
|
+
if (!isLaheluUrl(url)) {
|
|
91
|
+
return {
|
|
92
|
+
status: false,
|
|
93
|
+
platform: "lahelu",
|
|
94
|
+
message: "Invalid Lahelu URL. Make sure it's a valid post URL.",
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const postId = extractPostId(url);
|
|
99
|
+
if (!postId) {
|
|
100
|
+
return {
|
|
101
|
+
status: false,
|
|
102
|
+
platform: "lahelu",
|
|
103
|
+
message: "Could not extract post ID from URL.",
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const post = await fetchLaheluData(postId);
|
|
108
|
+
|
|
109
|
+
if (!post) {
|
|
110
|
+
return {
|
|
111
|
+
status: false,
|
|
112
|
+
platform: "lahelu",
|
|
113
|
+
message: "Post not found. The post may have been deleted or is private.",
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const medias = normalizeMedia(post);
|
|
118
|
+
|
|
119
|
+
if (medias.length === 0) {
|
|
120
|
+
return {
|
|
121
|
+
status: false,
|
|
122
|
+
platform: "lahelu",
|
|
123
|
+
message: "No media found in this post.",
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const title = post.title || "Untitled";
|
|
128
|
+
const author = post.userUsername || post.userInfo?.username || "Unknown";
|
|
129
|
+
const createdAt = post.createTime ? new Date(post.createTime * 1000).toLocaleDateString("en-US") : "-";
|
|
130
|
+
const views = parseInt(post.totalViews || 0, 10);
|
|
131
|
+
const likes = parseInt(post.totalUpvotes || 0, 10);
|
|
132
|
+
const comments = parseInt(post.totalComments || 0, 10);
|
|
133
|
+
|
|
134
|
+
const selected = medias[0];
|
|
135
|
+
const ext = selected.format.startsWith(".") ? selected.format : "." + selected.format;
|
|
136
|
+
const safeTitle = sanitizeFileName(postId);
|
|
137
|
+
const fileName = `${safeTitle}_${selected.type}${ext}`;
|
|
138
|
+
|
|
139
|
+
return {
|
|
140
|
+
status: true,
|
|
141
|
+
platform: "lahelu",
|
|
142
|
+
data: {
|
|
143
|
+
postId: post.postId || post.postID || postId,
|
|
144
|
+
title: title,
|
|
145
|
+
author: author,
|
|
146
|
+
createdAt: createdAt,
|
|
147
|
+
stats: {
|
|
148
|
+
views: views,
|
|
149
|
+
likes: likes,
|
|
150
|
+
comments: comments,
|
|
151
|
+
},
|
|
152
|
+
media: {
|
|
153
|
+
type: selected.type,
|
|
154
|
+
format: selected.format,
|
|
155
|
+
url: selected.url,
|
|
156
|
+
desc: selected.desc,
|
|
157
|
+
},
|
|
158
|
+
fileName: fileName,
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
} catch (error) {
|
|
162
|
+
return {
|
|
163
|
+
status: false,
|
|
164
|
+
platform: "lahelu",
|
|
165
|
+
message: error.message || "An unexpected error occurred",
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
module.exports = laheluDownloader;
|
package/src/lib/tiktok.js
CHANGED
|
@@ -7,17 +7,88 @@ try {
|
|
|
7
7
|
wrapper = require('axios-cookiejar-support').wrapper;
|
|
8
8
|
} catch (err) { }
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
function extractVideoId(url) {
|
|
11
|
+
const match = url.match(/\/(?:video|photo)\/(\d+)/);
|
|
12
|
+
return match ? match[1] : null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function tiktokViaTikwm(url) {
|
|
16
|
+
const res = await axios.post('https://www.tikwm.com/api/',
|
|
17
|
+
new URLSearchParams({ url, count: 12, cursor: 0, web: 1, hd: 1 }).toString(),
|
|
18
|
+
{
|
|
19
|
+
headers: {
|
|
20
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
21
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/135.0.0.0 Safari/537.36',
|
|
22
|
+
},
|
|
23
|
+
timeout: 15000,
|
|
24
|
+
}
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const apiData = res.data;
|
|
28
|
+
if (!apiData || apiData.code !== 0 || !apiData.data) {
|
|
29
|
+
throw new Error('tikwm API returned an error or empty data.');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const d = apiData.data;
|
|
33
|
+
const isImages = d.images && d.images.length > 0;
|
|
34
|
+
|
|
35
|
+
const importantData = {
|
|
36
|
+
videoId: d.id,
|
|
37
|
+
description: d.title,
|
|
38
|
+
createTime: d.create_time,
|
|
39
|
+
videoUrl: isImages ? null : (d.hdplay || d.play),
|
|
40
|
+
videoInfo: {
|
|
41
|
+
size: d.size || null,
|
|
42
|
+
duration: d.duration,
|
|
43
|
+
width: d.width || null,
|
|
44
|
+
height: d.height || null,
|
|
45
|
+
definition: d.hd_size ? 'hd' : 'sd',
|
|
46
|
+
coverUrl: d.cover || d.origin_cover,
|
|
47
|
+
subtitles: []
|
|
48
|
+
},
|
|
49
|
+
author: {
|
|
50
|
+
id: d.author?.id,
|
|
51
|
+
uniqueId: d.author?.unique_id,
|
|
52
|
+
nickname: d.author?.nickname,
|
|
53
|
+
avatarThumb: d.author?.avatar
|
|
54
|
+
},
|
|
55
|
+
music: {
|
|
56
|
+
id: d.music_info?.id,
|
|
57
|
+
title: d.music_info?.title,
|
|
58
|
+
authorName: d.music_info?.author,
|
|
59
|
+
playUrl: d.music_info?.play || d.music,
|
|
60
|
+
isOriginal: d.music_info?.original
|
|
61
|
+
},
|
|
62
|
+
stats: {
|
|
63
|
+
likes: d.digg_count,
|
|
64
|
+
shares: d.share_count,
|
|
65
|
+
comments: d.comment_count,
|
|
66
|
+
plays: d.play_count,
|
|
67
|
+
collects: d.collect_count,
|
|
68
|
+
reposts: null
|
|
69
|
+
},
|
|
70
|
+
locationCreated: null,
|
|
71
|
+
videoBuffer: null,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
if (isImages) {
|
|
75
|
+
importantData.images = d.images;
|
|
76
|
+
importantData.videoInfo.type = 'images';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
status: true,
|
|
81
|
+
platform: "tiktok",
|
|
82
|
+
data: importantData
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function tiktokViaDirect(url) {
|
|
11
87
|
if (!CookieJar || !wrapper) {
|
|
12
|
-
|
|
13
|
-
status: false,
|
|
14
|
-
platform: "tiktok",
|
|
15
|
-
message: 'tough-cookie and axios-cookiejar-support are required for TikTok downloads. Install them with: npm install tough-cookie axios-cookiejar-support'
|
|
16
|
-
};
|
|
88
|
+
throw new Error('Direct scraping requires tough-cookie and axios-cookiejar-support.');
|
|
17
89
|
}
|
|
18
90
|
|
|
19
91
|
const jar = new CookieJar();
|
|
20
|
-
|
|
21
92
|
const apiClient = axios.create({
|
|
22
93
|
jar: jar,
|
|
23
94
|
withCredentials: true,
|
|
@@ -27,93 +98,142 @@ async function tiktokDownloader(url) {
|
|
|
27
98
|
});
|
|
28
99
|
wrapper(apiClient);
|
|
29
100
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
definition: itemStruct.video?.definition,
|
|
55
|
-
coverUrl: itemStruct.video?.cover,
|
|
56
|
-
subtitles: itemStruct.video?.subtitleInfos?.map(sub => ({
|
|
57
|
-
language: sub.LanguageCodeName, url: sub.Url, format: sub.Format, source: sub.Source
|
|
58
|
-
})) || []
|
|
59
|
-
},
|
|
60
|
-
author: {
|
|
61
|
-
id: itemStruct.author?.id,
|
|
62
|
-
uniqueId: itemStruct.author?.uniqueId,
|
|
63
|
-
nickname: itemStruct.author?.nickname,
|
|
64
|
-
avatarThumb: itemStruct.author?.avatarThumb
|
|
65
|
-
},
|
|
66
|
-
music: {
|
|
67
|
-
id: itemStruct.music?.id,
|
|
68
|
-
title: itemStruct.music?.title,
|
|
69
|
-
authorName: itemStruct.music?.authorName,
|
|
70
|
-
playUrl: itemStruct.music?.playUrl,
|
|
71
|
-
isOriginal: itemStruct.music?.original
|
|
72
|
-
},
|
|
73
|
-
stats: {
|
|
74
|
-
likes: itemStruct.statsV2?.diggCount ?? itemStruct.stats?.diggCount,
|
|
75
|
-
shares: itemStruct.statsV2?.shareCount ?? itemStruct.stats?.shareCount,
|
|
76
|
-
comments: itemStruct.statsV2?.commentCount ?? itemStruct.stats?.commentCount,
|
|
77
|
-
plays: itemStruct.statsV2?.playCount ?? itemStruct.stats?.playCount,
|
|
78
|
-
collects: itemStruct.statsV2?.collectCount ?? itemStruct.stats?.collectCount,
|
|
79
|
-
reposts: itemStruct.statsV2?.repostCount
|
|
80
|
-
},
|
|
81
|
-
locationCreated: itemStruct.locationCreated,
|
|
82
|
-
videoBuffer: null
|
|
83
|
-
};
|
|
101
|
+
const htmlResponse = await apiClient.get(url);
|
|
102
|
+
const html = htmlResponse.data;
|
|
103
|
+
const $ = cheerio.load(html);
|
|
104
|
+
let itemStruct = null;
|
|
105
|
+
|
|
106
|
+
// Approach 1: __UNIVERSAL_DATA_FOR_REHYDRATION__
|
|
107
|
+
const universalData = $('#__UNIVERSAL_DATA_FOR_REHYDRATION__').html();
|
|
108
|
+
if (universalData) {
|
|
109
|
+
try {
|
|
110
|
+
const jsonData = JSON.parse(universalData);
|
|
111
|
+
itemStruct = jsonData?.__DEFAULT_SCOPE__?.["webapp.video-detail"]?.itemInfo?.itemStruct;
|
|
112
|
+
} catch { }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Approach 2: __NEXT_DATA__
|
|
116
|
+
if (!itemStruct) {
|
|
117
|
+
const nextData = $('#__NEXT_DATA__').html();
|
|
118
|
+
if (nextData) {
|
|
119
|
+
try {
|
|
120
|
+
const jsonData = JSON.parse(nextData);
|
|
121
|
+
itemStruct = jsonData?.props?.pageProps?.itemInfo?.itemStruct;
|
|
122
|
+
} catch { }
|
|
123
|
+
}
|
|
124
|
+
}
|
|
84
125
|
|
|
85
|
-
|
|
126
|
+
// Approach 3: SIGI_STATE script
|
|
127
|
+
if (!itemStruct) {
|
|
128
|
+
const sigiScript = $('script').filter((_, el) => {
|
|
129
|
+
const text = $(el).html() || '';
|
|
130
|
+
return text.includes('SIGI_STATE') || text.includes('"ItemModule"');
|
|
131
|
+
}).html();
|
|
132
|
+
if (sigiScript) {
|
|
86
133
|
try {
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
134
|
+
const match = sigiScript.match(/window\['SIGI_STATE'\]\s*=\s*(\{.+?\});/s)
|
|
135
|
+
|| sigiScript.match(/SIGI_STATE\s*=\s*(\{.+?\});/s);
|
|
136
|
+
if (match) {
|
|
137
|
+
const sigiData = JSON.parse(match[1]);
|
|
138
|
+
const itemModule = sigiData?.ItemModule;
|
|
139
|
+
if (itemModule) {
|
|
140
|
+
const firstKey = Object.keys(itemModule)[0];
|
|
141
|
+
if (firstKey) itemStruct = itemModule[firstKey];
|
|
92
142
|
}
|
|
93
|
-
}
|
|
143
|
+
}
|
|
144
|
+
} catch { }
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!itemStruct) throw new Error('Direct scraping failed.');
|
|
149
|
+
|
|
150
|
+
const videoUrlToDownload = itemStruct.video?.downloadAddr || itemStruct.video?.playAddr;
|
|
151
|
+
const videoId = itemStruct.id;
|
|
94
152
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
153
|
+
const importantData = {
|
|
154
|
+
videoId: videoId,
|
|
155
|
+
description: itemStruct.desc,
|
|
156
|
+
createTime: itemStruct.createTime,
|
|
157
|
+
videoUrl: videoUrlToDownload,
|
|
158
|
+
videoInfo: {
|
|
159
|
+
size: null,
|
|
160
|
+
duration: itemStruct.video?.duration,
|
|
161
|
+
width: itemStruct.video?.width,
|
|
162
|
+
height: itemStruct.video?.height,
|
|
163
|
+
definition: itemStruct.video?.definition,
|
|
164
|
+
coverUrl: itemStruct.video?.cover,
|
|
165
|
+
subtitles: itemStruct.video?.subtitleInfos?.map(sub => ({
|
|
166
|
+
language: sub.LanguageCodeName, url: sub.Url, format: sub.Format, source: sub.Source
|
|
167
|
+
})) || []
|
|
168
|
+
},
|
|
169
|
+
author: {
|
|
170
|
+
id: itemStruct.author?.id,
|
|
171
|
+
uniqueId: itemStruct.author?.uniqueId,
|
|
172
|
+
nickname: itemStruct.author?.nickname,
|
|
173
|
+
avatarThumb: itemStruct.author?.avatarThumb
|
|
174
|
+
},
|
|
175
|
+
music: {
|
|
176
|
+
id: itemStruct.music?.id,
|
|
177
|
+
title: itemStruct.music?.title,
|
|
178
|
+
authorName: itemStruct.music?.authorName,
|
|
179
|
+
playUrl: itemStruct.music?.playUrl,
|
|
180
|
+
isOriginal: itemStruct.music?.original
|
|
181
|
+
},
|
|
182
|
+
stats: {
|
|
183
|
+
likes: itemStruct.statsV2?.diggCount ?? itemStruct.stats?.diggCount,
|
|
184
|
+
shares: itemStruct.statsV2?.shareCount ?? itemStruct.stats?.shareCount,
|
|
185
|
+
comments: itemStruct.statsV2?.commentCount ?? itemStruct.stats?.commentCount,
|
|
186
|
+
plays: itemStruct.statsV2?.playCount ?? itemStruct.stats?.playCount,
|
|
187
|
+
collects: itemStruct.statsV2?.collectCount ?? itemStruct.stats?.collectCount,
|
|
188
|
+
reposts: itemStruct.statsV2?.repostCount
|
|
189
|
+
},
|
|
190
|
+
locationCreated: itemStruct.locationCreated,
|
|
191
|
+
videoBuffer: null
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
if (videoUrlToDownload) {
|
|
195
|
+
try {
|
|
196
|
+
const videoResponse = await apiClient.get(videoUrlToDownload, {
|
|
197
|
+
responseType: 'arraybuffer',
|
|
198
|
+
headers: {
|
|
199
|
+
'Referer': url,
|
|
200
|
+
'Range': 'bytes=0-'
|
|
98
201
|
}
|
|
99
|
-
}
|
|
100
|
-
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (videoResponse.status === 200 || videoResponse.status === 206) {
|
|
205
|
+
importantData.videoBuffer = Buffer.from(videoResponse.data);
|
|
206
|
+
importantData.videoInfo.size = videoResponse.data.length;
|
|
101
207
|
}
|
|
208
|
+
} catch (videoError) {
|
|
209
|
+
// Video download failed, but metadata is still available
|
|
102
210
|
}
|
|
211
|
+
}
|
|
103
212
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
213
|
+
return {
|
|
214
|
+
status: true,
|
|
215
|
+
platform: "tiktok",
|
|
216
|
+
data: importantData
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
async function tiktokDownloader(url) {
|
|
221
|
+
// Try direct scraping first
|
|
222
|
+
try {
|
|
223
|
+
return await tiktokViaDirect(url);
|
|
224
|
+
} catch { }
|
|
109
225
|
|
|
226
|
+
// Fallback: tikwm.com API
|
|
227
|
+
try {
|
|
228
|
+
return await tiktokViaTikwm(url);
|
|
110
229
|
} catch (error) {
|
|
111
230
|
return {
|
|
112
231
|
status: false,
|
|
113
232
|
platform: "tiktok",
|
|
114
|
-
message: error.message || '
|
|
233
|
+
message: error.message || 'All TikTok scraping methods failed.'
|
|
115
234
|
};
|
|
116
235
|
}
|
|
117
236
|
}
|
|
118
237
|
|
|
119
238
|
module.exports = tiktokDownloader;
|
|
239
|
+
|
package/src/lib/twitter.js
CHANGED
|
@@ -59,14 +59,31 @@ async function getTokens(tweetUrl) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
const { data: redirectPageData } = await axios.get(tweetUrl, { headers: DEFAULT_HEADERS });
|
|
62
|
-
const mainJsUrlMatch = redirectPageData.match(/https:\/\/abs.twimg.com\/responsive-web\/client-web-legacy\/main\.[^\.]+\.js/);
|
|
63
62
|
|
|
64
|
-
|
|
65
|
-
|
|
63
|
+
// Try multiple patterns for the main JS bundle URL
|
|
64
|
+
const mainJsPatterns = [
|
|
65
|
+
/https:\/\/abs\.twimg\.com\/responsive-web\/client-web-legacy\/main\.[^\.]+\.js/,
|
|
66
|
+
/https:\/\/abs\.twimg\.com\/responsive-web\/client-web\/main\.[^\.]+\.js/,
|
|
67
|
+
/https:\/\/abs\.twimg\.com\/responsive-web\/[^\/]+\/main\.[^"'\s]+\.js/,
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
let bearerToken = null;
|
|
71
|
+
|
|
72
|
+
for (const pattern of mainJsPatterns) {
|
|
73
|
+
const match = redirectPageData.match(pattern);
|
|
74
|
+
if (match) {
|
|
75
|
+
try {
|
|
76
|
+
bearerToken = await extractBearerToken(match[0]);
|
|
77
|
+
break;
|
|
78
|
+
} catch { }
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Fallback: use the well-known public guest bearer token
|
|
83
|
+
if (!bearerToken) {
|
|
84
|
+
bearerToken = 'AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA';
|
|
66
85
|
}
|
|
67
86
|
|
|
68
|
-
const mainJsUrl = mainJsUrlMatch[0];
|
|
69
|
-
const bearerToken = await extractBearerToken(mainJsUrl);
|
|
70
87
|
const guestToken = await getGuestToken(redirectPageData);
|
|
71
88
|
const tokens = { bearer: bearerToken, guest: guestToken };
|
|
72
89
|
tokenCache.set(tweetUrl, tokens);
|