@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # @lmna22/aio-downloader
2
2
 
3
- > All-in-one media downloader for YouTube, Instagram, TikTok, Pinterest, Pixiv, and X/Twitter.
3
+ > All-in-one media downloader for YouTube, Instagram, TikTok, Pinterest, Pixiv, X/Twitter, and Lahelu.
4
4
 
5
5
  [![npm version](https://img.shields.io/npm/v/@lmna22/aio-downloader.svg)](https://www.npmjs.com/package/@lmna22/aio-downloader)
6
6
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](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 different methods for maximum reliability
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
- // Auto-detects the platform from the URL
56
- const result = await aioDownloader("https://www.youtube.com/watch?v=dQw4w9WgXcQ", { quality: 5 });
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 { youtubeDownloader, youtubePlaylistDownloader } = require("@lmna22/aio-downloader");
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 youtubeDownloader("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 5);
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 youtubeDownloader("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 8);
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 youtubeDownloader("https://www.youtube.com/watch?v=dQw4w9WgXcQ", 9);
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 youtubePlaylistDownloader(
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 { instagramDownloader } = require("@lmna22/aio-downloader");
112
+ const { lmna } = require("@lmna22/aio-downloader");
108
113
 
109
- const result = await instagramDownloader("https://www.instagram.com/p/ABC123/");
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 { tiktokDownloader } = require("@lmna22/aio-downloader");
135
+ const { lmna } = require("@lmna22/aio-downloader");
131
136
 
132
- const result = await tiktokDownloader("https://www.tiktok.com/@user/video/1234567890");
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 { pinterestDownloader } = require("@lmna22/aio-downloader");
169
+ const { lmna } = require("@lmna22/aio-downloader");
160
170
 
161
171
  // From a direct pin URL
162
- const data = await pinterestDownloader("https://www.pinterest.com/pin/123456789/");
172
+ const data = await lmna.pinterest("https://www.pinterest.com/pin/123456789/");
163
173
 
164
174
  // Or search by keyword
165
- const searchResults = await pinterestDownloader("aesthetic wallpaper", { limit: 20 });
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 { pixivDownloader } = require("@lmna22/aio-downloader");
192
+ const { lmna } = require("@lmna22/aio-downloader");
183
193
 
184
194
  // From an artwork URL
185
- const data = await pixivDownloader("https://www.pixiv.net/artworks/12345678");
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 pixivDownloader("landscape", { limit: 5 });
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 pixivDownloader("landscape", { limit: 10, enrich: false });
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 { twitterDownloader } = require("@lmna22/aio-downloader");
221
+ const { lmna } = require("@lmna22/aio-downloader");
212
222
 
213
- const result = await twitterDownloader("https://x.com/user/status/1234567890");
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
- ### `youtubeDownloader(url, quality)`
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:** `{ creator, status, data: { title, result (Buffer), size, quality, desc, views, likes, channel, uploadDate, thumb, type } }`
368
+ **Returns:** `{ status, platform, data: { title, result (Buffer), size, quality, desc, views, likes, channel, uploadDate, thumb, type } }`
312
369
 
313
370
  ---
314
371
 
315
- ### `youtubePlaylistDownloader(url, quality, folderPath?)`
372
+ ### `lmna.youtubePlaylist(url, quality, folderPath?)`
316
373
 
317
374
  Downloads all videos from a YouTube playlist.
318
375
 
319
- **Returns:** `{ creator, status, data: { title, resultPath[], metadata[] } }`
376
+ **Returns:** `{ status, platform, data: { title, resultPath[], metadata[] } }`
320
377
 
321
378
  ---
322
379
 
323
- ### `instagramDownloader(url)`
380
+ ### `lmna.instagram(url)`
324
381
 
325
382
  Uses 4 fallback methods for maximum reliability.
326
383
 
327
- **Returns:** `{ creator, status, data: { url[], caption, username, like, comment, isVideo } }`
384
+ **Returns:** `{ status, platform, data: { url[], caption, username, like, comment, isVideo } }`
328
385
 
329
386
  ---
330
387
 
331
- ### `tiktokDownloader(url)`
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:** `{ creator, status, data: { videoId, description, videoUrl, videoBuffer (Buffer), videoInfo, author, music, stats, locationCreated } }`
392
+ **Returns:** `{ status, platform, data: { videoId, description, videoUrl, videoBuffer (Buffer), videoInfo, author, music, stats, locationCreated, images? } }`
336
393
 
337
394
  ---
338
395
 
339
- ### `pinterestDownloader(input, options?)`
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
- ### `pixivDownloader(input, options?)`
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
- ### `twitterDownloader(url)`
419
+ ### `lmna.twitter(url)`
363
420
 
364
421
  Extracts best quality video/image/gif from tweets via Twitter GraphQL API.
365
422
 
366
- **Returns:** `{ creator, status, data: { author, like, view, retweet, description, sensitiveContent, result: [{ type, url, thumb? }] } }`
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 { youtubeDownloader } = require("@lmna22/aio-downloader");
478
+ const { lmna } = require("@lmna22/aio-downloader");
398
479
 
399
- const result = await youtubeDownloader("https://www.youtube.com/watch?v=invalid", 5);
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": "1.0.3",
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 aio = require('./src/index.js'); console.log('Exports:', Object.keys(aio)); console.log('Platform detection:', aio.detectPlatform('https://youtube.com/watch?v=test'));\""
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\./i.test(url) },
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
- async function tiktokDownloader(url) {
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
- return {
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
- try {
31
- const htmlResponse = await apiClient.get(url);
32
- const html = htmlResponse.data;
33
-
34
- const $ = cheerio.load(html);
35
- const scriptContent = $('#__UNIVERSAL_DATA_FOR_REHYDRATION__').html();
36
- if (!scriptContent) throw new Error('Script tag #__UNIVERSAL_DATA_FOR_REHYDRATION__ not found.');
37
- const jsonData = JSON.parse(scriptContent);
38
- const itemStruct = jsonData?.__DEFAULT_SCOPE__?.["webapp.video-detail"]?.itemInfo?.itemStruct;
39
- if (!itemStruct) throw new Error('itemStruct not found within the JSON data.');
40
-
41
- const videoUrlToDownload = itemStruct.video?.downloadAddr || itemStruct.video?.playAddr;
42
- const videoId = itemStruct.id;
43
-
44
- const importantData = {
45
- videoId: videoId,
46
- description: itemStruct.desc,
47
- createTime: itemStruct.createTime,
48
- videoUrl: videoUrlToDownload,
49
- videoInfo: {
50
- size: null,
51
- duration: itemStruct.video?.duration,
52
- width: itemStruct.video?.width,
53
- height: itemStruct.video?.height,
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
- if (videoUrlToDownload) {
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 videoResponse = await apiClient.get(videoUrlToDownload, {
88
- responseType: 'arraybuffer',
89
- headers: {
90
- 'Referer': url,
91
- 'Range': 'bytes=0-'
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
- if (videoResponse.status === 200 || videoResponse.status === 206) {
96
- importantData.videoBuffer = Buffer.from(videoResponse.data);
97
- importantData.videoInfo.size = videoResponse.data.length;
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
- } catch (videoError) {
100
- console.error(`Error downloading video: ${videoError.message}`);
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
- return {
105
- status: true,
106
- platform: "tiktok",
107
- data: importantData
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 || 'An unexpected error occurred'
233
+ message: error.message || 'All TikTok scraping methods failed.'
115
234
  };
116
235
  }
117
236
  }
118
237
 
119
238
  module.exports = tiktokDownloader;
239
+
@@ -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
- if (!mainJsUrlMatch) {
65
- throw new Error('Failed to find main JS URL.');
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);
@@ -33,6 +33,7 @@ async function youtubeDownloader(link, qualityIndex) {
33
33
  if (!youtubedl) {
34
34
  return {
35
35
  status: false,
36
+ platform: "youtube",
36
37
  message: `youtube-dl-exec not found, can't download video`,
37
38
  };
38
39
  }