@lmna22/aio-downloader 2.0.1 → 2.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +92 -2
- package/package.json +6 -10
- package/src/index.js +13 -1
- package/src/lib/dailymotion.js +278 -0
- package/src/lib/spotify.js +89 -0
- package/src/lib/xiaohongshu.js +288 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @lmna22/aio-downloader
|
|
2
2
|
|
|
3
|
-
> All-in-one media downloader for YouTube, Instagram, TikTok, Pinterest, Pixiv, X
|
|
3
|
+
> All-in-one media downloader for YouTube, Instagram, TikTok, Pinterest, Pixiv, X (Twitter), Lahelu, Xiaohongshu (RedNote), and more.
|
|
4
4
|
|
|
5
5
|
[](https://www.npmjs.com/package/@lmna22/aio-downloader)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
@@ -21,6 +21,9 @@ 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** | ✅ | — | ✅ | — |
|
|
25
|
+
| **Dailymotion** | ✅ | ✅ | ✅ | ✅ |
|
|
26
|
+
| **Spotify** | — | ✅ (MP3, Opus, WAV) | — | — |
|
|
24
27
|
|
|
25
28
|
- 🔗 **Auto-detect platform** from URL — just pass any supported link
|
|
26
29
|
- 📦 **Programmatic API** — designed for Node.js applications, bots, and scripts
|
|
@@ -95,7 +98,77 @@ const { lmna } = require("@lmna22/aio-downloader");
|
|
|
95
98
|
const result = await lmna.tiktok("https://www.tiktok.com/@user/video/1234567890");
|
|
96
99
|
```
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
### Xiaohongshu / RedNote
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
105
|
+
|
|
106
|
+
const result = await lmna.xiaohongshu("https://www.xiaohongshu.com/explore/abc123");
|
|
107
|
+
|
|
108
|
+
if (result.status) {
|
|
109
|
+
console.log(result.data.title);
|
|
110
|
+
console.log(result.data.author.nickname);
|
|
111
|
+
console.log(result.data.stats.likes);
|
|
112
|
+
console.log(result.data.stats.collects);
|
|
113
|
+
console.log(result.data.stats.comments);
|
|
114
|
+
console.log(result.data.media.url);
|
|
115
|
+
|
|
116
|
+
// Download the media
|
|
117
|
+
const { download } = require("@lmna22/aio-downloader");
|
|
118
|
+
await download(result.data.media.url, `./downloads/${result.data.fileName}`);
|
|
119
|
+
}
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### Dailymotion
|
|
123
|
+
|
|
124
|
+
```javascript
|
|
125
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
126
|
+
|
|
127
|
+
// Download video
|
|
128
|
+
const result = await lmna.dailymotion("https://www.dailymotion.com/video/x8z3v2y");
|
|
129
|
+
|
|
130
|
+
if (result.status) {
|
|
131
|
+
console.log(result.data.title);
|
|
132
|
+
console.log(result.data.author);
|
|
133
|
+
console.log(result.data.views);
|
|
134
|
+
console.log(result.data.url);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Search videos
|
|
138
|
+
const searchResult = await lmna.dailymotion("https://www.dailymotion.com/search/programming", { query: "programming", limit: 10 });
|
|
139
|
+
|
|
140
|
+
if (searchResult.status) {
|
|
141
|
+
console.log(searchResult.data.videos);
|
|
142
|
+
}
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
### Spotify
|
|
146
|
+
|
|
147
|
+
```javascript
|
|
148
|
+
const { lmna } = require("@lmna22/aio-downloader");
|
|
149
|
+
|
|
150
|
+
// Get track metadata (returns search query, does not download)
|
|
151
|
+
const result = await lmna.spotify("https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT");
|
|
152
|
+
|
|
153
|
+
if (result.status) {
|
|
154
|
+
console.log(result.data.title);
|
|
155
|
+
console.log(result.data.artist);
|
|
156
|
+
console.log(result.data.cover);
|
|
157
|
+
console.log(result.data.searchQuery); // Use this to download with yt-dlp
|
|
158
|
+
|
|
159
|
+
// Download using yt-dlp or similar tool
|
|
160
|
+
// Example: yt-dlp "ytsearch1:Never Gonna Give You Up audio" --extract-audio --audio-format mp3
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Supported formats: mp3, opus, wav
|
|
164
|
+
const mp3Result = await lmna.spotify("https://open.spotify.com/track/abc123", { format: 'mp3' });
|
|
165
|
+
const opusResult = await lmna.spotify("https://open.spotify.com/track/abc123", { format: 'opus' });
|
|
166
|
+
const wavResult = await lmna.spotify("https://open.spotify.com/track/abc123", { format: 'wav' });
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
**Note:** Spotify scraper returns metadata and a search query. Use yt-dlp or similar tool to download the actual audio file using the provided searchQuery.
|
|
170
|
+
|
|
171
|
+
*(See the API Reference below for other platforms: Pinterest, Pixiv, Twitter, Lahelu, Dailymotion)*
|
|
99
172
|
|
|
100
173
|
---
|
|
101
174
|
|
|
@@ -132,6 +205,12 @@ await aioDownloader("https://www.pinterest.com/pin/456/");
|
|
|
132
205
|
await aioDownloader("https://www.pixiv.net/artworks/789");
|
|
133
206
|
await aioDownloader("https://x.com/user/status/101112");
|
|
134
207
|
await aioDownloader("https://lahelu.com/post/abc123");
|
|
208
|
+
await aioDownloader("https://www.xiaohongshu.com/explore/abc123");
|
|
209
|
+
await aioDownloader("https://www.rednote.com/explore/abc123");
|
|
210
|
+
await aioDownloader("https://xhslink.com/abc123");
|
|
211
|
+
await aioDownloader("https://www.dailymotion.com/video/x8z3v2y");
|
|
212
|
+
await aioDownloader("https://www.dailymotion.com/search/programming");
|
|
213
|
+
await aioDownloader("https://open.spotify.com/track/4cOdK2wGLETKBW3PvgPWqT");
|
|
135
214
|
|
|
136
215
|
// Force a specific platform
|
|
137
216
|
await aioDownloader("https://example.com/video", { platform: "youtube", quality: 5 });
|
|
@@ -145,6 +224,12 @@ detectPlatform("https://pin.it/abc123"); // "pinterest"
|
|
|
145
224
|
detectPlatform("https://pixiv.net/artworks/456"); // "pixiv"
|
|
146
225
|
detectPlatform("https://x.com/user/status/789"); // "twitter"
|
|
147
226
|
detectPlatform("https://lahelu.com/post/abc"); // "lahelu"
|
|
227
|
+
detectPlatform("https://xiaohongshu.com/explore/abc"); // "xiaohongshu"
|
|
228
|
+
detectPlatform("https://rednote.com/explore/abc"); // "xiaohongshu"
|
|
229
|
+
detectPlatform("https://xhslink.com/abc"); // "xiaohongshu"
|
|
230
|
+
detectPlatform("https://dailymotion.com/video/x8z3v2y"); // "dailymotion"
|
|
231
|
+
detectPlatform("https://dailymotion.com/search/music"); // "dailymotion"
|
|
232
|
+
detectPlatform("https://open.spotify.com/track/abc123"); // "spotify"
|
|
148
233
|
detectPlatform("https://unknown.com"); // null
|
|
149
234
|
```
|
|
150
235
|
|
|
@@ -167,6 +252,11 @@ await lmna.pinterest(url, options);
|
|
|
167
252
|
await lmna.pixiv(url, options);
|
|
168
253
|
await lmna.twitter(url);
|
|
169
254
|
await lmna.lahelu(url);
|
|
255
|
+
await lmna.xiaohongshu(url);
|
|
256
|
+
await lmna.dailymotion(url, options);
|
|
257
|
+
await lmna.spotify(url, options);
|
|
258
|
+
|
|
259
|
+
// All return: { status, platform, data?, message? }
|
|
170
260
|
```
|
|
171
261
|
|
|
172
262
|
---
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmna22/aio-downloader",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.3",
|
|
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": {
|
|
@@ -20,7 +20,10 @@
|
|
|
20
20
|
"image",
|
|
21
21
|
"media",
|
|
22
22
|
"scraper",
|
|
23
|
-
"aio"
|
|
23
|
+
"aio",
|
|
24
|
+
"lahelu",
|
|
25
|
+
"dailymotion",
|
|
26
|
+
"xiaohongshu"
|
|
24
27
|
],
|
|
25
28
|
"author": "",
|
|
26
29
|
"license": "MIT",
|
|
@@ -37,18 +40,11 @@
|
|
|
37
40
|
"axios-cookiejar-support": "^5.0.0",
|
|
38
41
|
"cheerio": "^1.0.0",
|
|
39
42
|
"ffmpeg-static": "^5.0.0",
|
|
43
|
+
"puppeteer": "^24.40.0",
|
|
40
44
|
"qs": "^6.13.0",
|
|
41
45
|
"tough-cookie": "^5.1.0",
|
|
42
46
|
"youtube-dl-exec": "^3.1.4"
|
|
43
47
|
},
|
|
44
|
-
"peerDependencies": {
|
|
45
|
-
"puppeteer": ">=20.0.0"
|
|
46
|
-
},
|
|
47
|
-
"peerDependenciesMeta": {
|
|
48
|
-
"puppeteer": {
|
|
49
|
-
"optional": true
|
|
50
|
-
}
|
|
51
|
-
},
|
|
52
48
|
"engines": {
|
|
53
49
|
"node": ">=14.0.0"
|
|
54
50
|
}
|
package/src/index.js
CHANGED
|
@@ -5,6 +5,9 @@ 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");
|
|
9
|
+
const dailymotionDownloader = require("./lib/dailymotion");
|
|
10
|
+
const spotifyDownloader = require("./lib/spotify");
|
|
8
11
|
const download = require("./download");
|
|
9
12
|
|
|
10
13
|
const PLATFORM_PATTERNS = [
|
|
@@ -15,6 +18,9 @@ const PLATFORM_PATTERNS = [
|
|
|
15
18
|
{ name: "pixiv", test: (url) => /pixiv\.net\//i.test(url) },
|
|
16
19
|
{ name: "twitter", test: (url) => /(?:twitter\.com\/|x\.com\/)/i.test(url) },
|
|
17
20
|
{ name: "lahelu", test: (url) => /lahelu\.com\/post\//i.test(url) },
|
|
21
|
+
{ name: "xiaohongshu", test: (url) => /xiaohongshu\.com|rednote\.com|xhslink\.com/i.test(url) },
|
|
22
|
+
{ name: "dailymotion", test: (url) => /dailymotion\.com\//i.test(url) },
|
|
23
|
+
{ name: "spotify", test: (url) => /open\.spotify\.com\//i.test(url) },
|
|
18
24
|
];
|
|
19
25
|
|
|
20
26
|
function detectPlatform(url) {
|
|
@@ -32,6 +38,9 @@ const scrapers = {
|
|
|
32
38
|
pixiv: (url, options) => pixivDownloader(url, options),
|
|
33
39
|
twitter: (url) => twitterDownloader(url),
|
|
34
40
|
lahelu: (url) => laheluDownloader(url),
|
|
41
|
+
xiaohongshu: (url) => xiaohongshuDownloader(url),
|
|
42
|
+
dailymotion: (url, options) => dailymotionDownloader(url, options),
|
|
43
|
+
spotify: (url, options) => spotifyDownloader(url, options),
|
|
35
44
|
};
|
|
36
45
|
|
|
37
46
|
async function aioDownloader(url, options = {}) {
|
|
@@ -39,7 +48,7 @@ async function aioDownloader(url, options = {}) {
|
|
|
39
48
|
|
|
40
49
|
if (!platform) {
|
|
41
50
|
throw new Error(
|
|
42
|
-
`Unsupported or unrecognized URL: ${url}. Supported platforms: YouTube, Instagram, TikTok, Pinterest, Pixiv, X/Twitter.`
|
|
51
|
+
`Unsupported or unrecognized URL: ${url}. Supported platforms: YouTube, Instagram, TikTok, Pinterest, Pixiv, X/Twitter, Dailymotion, Spotify.`
|
|
43
52
|
);
|
|
44
53
|
}
|
|
45
54
|
|
|
@@ -60,6 +69,9 @@ const lmna = {
|
|
|
60
69
|
pixiv: (url, options) => pixivDownloader(url, options),
|
|
61
70
|
twitter: (url) => twitterDownloader(url),
|
|
62
71
|
lahelu: (url) => laheluDownloader(url),
|
|
72
|
+
xiaohongshu: (url) => xiaohongshuDownloader(url),
|
|
73
|
+
dailymotion: (url, options) => dailymotionDownloader(url, options),
|
|
74
|
+
spotify: (url, options) => spotifyDownloader(url, options),
|
|
63
75
|
};
|
|
64
76
|
|
|
65
77
|
module.exports = {
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
const puppeteer = require('puppeteer');
|
|
2
|
+
|
|
3
|
+
function extractVideoId(url) {
|
|
4
|
+
const patterns = [
|
|
5
|
+
/dailymotion\.com\/video\/([a-zA-Z0-9]+)/,
|
|
6
|
+
/dai\.ly\/([a-zA-Z0-9]+)/,
|
|
7
|
+
/dailymotion\.com\/embed\/video\/([a-zA-Z0-9]+)/,
|
|
8
|
+
];
|
|
9
|
+
|
|
10
|
+
for (const pattern of patterns) {
|
|
11
|
+
const match = url.match(pattern);
|
|
12
|
+
if (match) return match[1];
|
|
13
|
+
}
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async function fetchDailymotionData(url) {
|
|
18
|
+
const videoId = extractVideoId(url);
|
|
19
|
+
if (!videoId) {
|
|
20
|
+
throw new Error('Invalid Dailymotion URL');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
let browser;
|
|
24
|
+
try {
|
|
25
|
+
browser = await puppeteer.launch({
|
|
26
|
+
headless: 'new',
|
|
27
|
+
args: ['--no-sandbox', '--disable-setuid-sandbox', '--disable-gpu']
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const page = await browser.newPage();
|
|
31
|
+
await page.setUserAgent('Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36');
|
|
32
|
+
|
|
33
|
+
let videoData = null;
|
|
34
|
+
let masterM3U8Url = null;
|
|
35
|
+
let masterM3U8Content = null;
|
|
36
|
+
|
|
37
|
+
page.on('response', async (response) => {
|
|
38
|
+
const resUrl = response.url();
|
|
39
|
+
|
|
40
|
+
if (resUrl.includes('geo.dailymotion.com') && resUrl.includes('/video/') && resUrl.includes('.json') && !resUrl.includes('fields=')) {
|
|
41
|
+
try {
|
|
42
|
+
const data = await response.json();
|
|
43
|
+
if (data && data.id === videoId) {
|
|
44
|
+
videoData = data;
|
|
45
|
+
}
|
|
46
|
+
} catch (e) { }
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (resUrl.includes('cdndirector.dailymotion.com') && resUrl.includes('.m3u8') && !resUrl.includes('cookie_sync') && !resUrl.includes('af=')) {
|
|
50
|
+
try {
|
|
51
|
+
const text = await response.text();
|
|
52
|
+
if (text.includes('#EXTM3U') && text.includes('#EXT-X-STREAM-INF')) {
|
|
53
|
+
masterM3U8Url = resUrl;
|
|
54
|
+
masterM3U8Content = text;
|
|
55
|
+
}
|
|
56
|
+
} catch (e) { }
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (resUrl.includes('graphql-eu-west-1.api.dailymotion.com') && resUrl.includes('video')) {
|
|
60
|
+
try {
|
|
61
|
+
const data = await response.json();
|
|
62
|
+
if (data && data.data && data.data.video && data.data.video.xid === videoId) {
|
|
63
|
+
const v = data.data.video;
|
|
64
|
+
videoData = {
|
|
65
|
+
id: v.xid || v.id,
|
|
66
|
+
title: v.title,
|
|
67
|
+
description: v.description,
|
|
68
|
+
duration: v.duration,
|
|
69
|
+
author: v.author,
|
|
70
|
+
thumbnails: {
|
|
71
|
+
'360': v.thumbnailx360,
|
|
72
|
+
'480': v.thumbnailx480,
|
|
73
|
+
'720': v.thumbnailx720
|
|
74
|
+
},
|
|
75
|
+
created_time: v.createdAt ? Math.floor(new Date(v.createdAt).getTime() / 1000) : null,
|
|
76
|
+
views_total: v.views,
|
|
77
|
+
likes_total: v.likes
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
} catch (e) { }
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
await page.goto(url, {
|
|
85
|
+
waitUntil: 'networkidle0',
|
|
86
|
+
timeout: 30000
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
await new Promise(r => setTimeout(r, 5000));
|
|
90
|
+
|
|
91
|
+
if (!videoData) {
|
|
92
|
+
videoData = await page.evaluate((id) => {
|
|
93
|
+
const content = document.documentElement.outerHTML;
|
|
94
|
+
|
|
95
|
+
const jsonPattern = new RegExp('{"id":"[^"]*' + id + '[^}]*"title":"[^"]+"[^}]*}', 'g');
|
|
96
|
+
const matches = content.match(jsonPattern);
|
|
97
|
+
|
|
98
|
+
if (matches && matches.length > 0) {
|
|
99
|
+
for (const match of matches) {
|
|
100
|
+
try {
|
|
101
|
+
const data = JSON.parse(match);
|
|
102
|
+
if (data.id === id && data.title) {
|
|
103
|
+
return {
|
|
104
|
+
id: data.id,
|
|
105
|
+
title: data.title,
|
|
106
|
+
description: data.description || '',
|
|
107
|
+
duration: data.duration || 0,
|
|
108
|
+
author: data.author || '',
|
|
109
|
+
thumbnails: data.thumbnails || {},
|
|
110
|
+
created_time: data.created_time,
|
|
111
|
+
views_total: data.views_total,
|
|
112
|
+
likes_total: data.likes_total
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
} catch (e) { }
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}, videoId);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
if (!videoData) {
|
|
123
|
+
throw new Error('Failed to fetch video metadata');
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!masterM3U8Content || !masterM3U8Content.includes('#EXT-X-STREAM-INF')) {
|
|
127
|
+
throw new Error('No video stream found');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
await browser.close();
|
|
131
|
+
|
|
132
|
+
const lines = masterM3U8Content.split('\n');
|
|
133
|
+
const streams = [];
|
|
134
|
+
let currentBandwidth = 0;
|
|
135
|
+
let currentResolution = '';
|
|
136
|
+
let currentName = '';
|
|
137
|
+
|
|
138
|
+
for (let i = 0; i < lines.length; i++) {
|
|
139
|
+
const line = lines[i].trim();
|
|
140
|
+
if (line.startsWith('#EXT-X-STREAM-INF:')) {
|
|
141
|
+
const bwMatch = line.match(/BANDWIDTH=(\d+)/);
|
|
142
|
+
const resMatch = line.match(/RESOLUTION=(\d+x\d+)/);
|
|
143
|
+
const nameMatch = line.match(/NAME="([^"]+)"/);
|
|
144
|
+
|
|
145
|
+
currentBandwidth = bwMatch ? parseInt(bwMatch[1]) : 0;
|
|
146
|
+
currentResolution = resMatch ? resMatch[1] : '';
|
|
147
|
+
currentName = nameMatch ? nameMatch[1] : '';
|
|
148
|
+
} else if (line && !line.startsWith('#') && (line.includes('.m3u8') || line.includes('dmcdn'))) {
|
|
149
|
+
if (currentResolution) {
|
|
150
|
+
const [width, height] = currentResolution.split('x').map(Number);
|
|
151
|
+
const fullUrl = line.startsWith('http') ? line : masterM3U8Url.substring(0, masterM3U8Url.lastIndexOf('/') + 1) + line;
|
|
152
|
+
streams.push({
|
|
153
|
+
quality: currentName ? `${currentName}p` : `${height}p`,
|
|
154
|
+
height: height,
|
|
155
|
+
width: width,
|
|
156
|
+
bandwidth: currentBandwidth,
|
|
157
|
+
name: currentName,
|
|
158
|
+
fps: null,
|
|
159
|
+
formatId: currentResolution,
|
|
160
|
+
type: 'MP4',
|
|
161
|
+
url: fullUrl,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
currentResolution = '';
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (streams.length === 0) {
|
|
169
|
+
throw new Error('No video qualities found in manifest');
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
streams.sort((a, b) => a.height - b.height);
|
|
173
|
+
|
|
174
|
+
streams.push({
|
|
175
|
+
quality: 'Audio Only',
|
|
176
|
+
height: 0,
|
|
177
|
+
width: 0,
|
|
178
|
+
bandwidth: streams[0].bandwidth,
|
|
179
|
+
name: 'Audio',
|
|
180
|
+
fps: null,
|
|
181
|
+
formatId: 'audio',
|
|
182
|
+
type: 'MP3',
|
|
183
|
+
url: streams[0].url,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const metadata = {
|
|
187
|
+
id: videoData.id || videoId,
|
|
188
|
+
title: videoData.title || 'Unknown',
|
|
189
|
+
description: videoData.description || '',
|
|
190
|
+
duration: videoData.duration || 0,
|
|
191
|
+
views: parseInt(videoData.views_total) || 0,
|
|
192
|
+
likes: parseInt(videoData.likes_total) || 0,
|
|
193
|
+
owner: {
|
|
194
|
+
screenname: videoData.owner?.screenname || videoData.author || '',
|
|
195
|
+
username: videoData.owner?.username || '',
|
|
196
|
+
id: videoData.owner?.id || '',
|
|
197
|
+
},
|
|
198
|
+
thumbnail: videoData.thumbnails?.['720'] || videoData.thumbnails?.['480'] || videoData.thumbnails?.['360'] || '',
|
|
199
|
+
uploadDate: videoData.created_time ? new Date(videoData.created_time * 1000).toISOString() : '',
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
metadata,
|
|
204
|
+
streams,
|
|
205
|
+
info: videoData,
|
|
206
|
+
};
|
|
207
|
+
} catch (err) {
|
|
208
|
+
if (browser) await browser.close().catch(() => { });
|
|
209
|
+
throw err;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
async function dailymotionDownloader(url) {
|
|
214
|
+
if (typeof url !== 'string' || (!url.includes('dailymotion.com') && !url.includes('dai.ly'))) {
|
|
215
|
+
return {
|
|
216
|
+
status: false,
|
|
217
|
+
platform: "dailymotion",
|
|
218
|
+
message: 'Invalid Dailymotion URL'
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
const data = await fetchDailymotionData(url);
|
|
224
|
+
|
|
225
|
+
if (!data || !data.metadata || !data.streams || data.streams.length === 0) {
|
|
226
|
+
return {
|
|
227
|
+
status: false,
|
|
228
|
+
platform: "dailymotion",
|
|
229
|
+
message: 'Failed to get video data'
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const { metadata, streams } = data;
|
|
234
|
+
|
|
235
|
+
const videoUrls = streams
|
|
236
|
+
.filter(s => s.quality !== 'Audio Only')
|
|
237
|
+
.map(s => s.url);
|
|
238
|
+
|
|
239
|
+
const audioUrl = streams.find(s => s.quality === 'Audio Only')?.url;
|
|
240
|
+
|
|
241
|
+
const qualities = streams
|
|
242
|
+
.filter(s => s.quality !== 'Audio Only')
|
|
243
|
+
.map(s => ({
|
|
244
|
+
quality: s.quality,
|
|
245
|
+
resolution: `${s.width}x${s.height}`,
|
|
246
|
+
bandwidth: s.bandwidth,
|
|
247
|
+
url: s.url
|
|
248
|
+
}));
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
status: true,
|
|
252
|
+
platform: "dailymotion",
|
|
253
|
+
data: {
|
|
254
|
+
id: metadata.id,
|
|
255
|
+
title: metadata.title,
|
|
256
|
+
description: metadata.description,
|
|
257
|
+
duration: metadata.duration,
|
|
258
|
+
views: metadata.views,
|
|
259
|
+
likes: metadata.likes,
|
|
260
|
+
author: metadata.owner?.screenname || metadata.owner?.username,
|
|
261
|
+
thumbnail: metadata.thumbnail,
|
|
262
|
+
uploadDate: metadata.uploadDate,
|
|
263
|
+
url: videoUrls.length > 0 ? videoUrls : [audioUrl],
|
|
264
|
+
audio: audioUrl || null,
|
|
265
|
+
qualities: qualities,
|
|
266
|
+
isVideo: true
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
} catch (error) {
|
|
270
|
+
return {
|
|
271
|
+
status: false,
|
|
272
|
+
platform: "dailymotion",
|
|
273
|
+
message: error.message
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = dailymotionDownloader;
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const youtubedl = require('youtube-dl-exec');
|
|
3
|
+
|
|
4
|
+
async function getSpotifyTrackInfo(url) {
|
|
5
|
+
try {
|
|
6
|
+
const trackId = url.split('track/')[1]?.split('?')[0] || url.split('track/')[1]?.split('/')[0];
|
|
7
|
+
|
|
8
|
+
if (!trackId) {
|
|
9
|
+
throw new Error('Invalid Spotify track URL');
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const { data } = await axios.get(`https://open.spotify.com/oembed?url=${encodeURIComponent(url)}`, {
|
|
13
|
+
headers: {
|
|
14
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36'
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
let title = data.title || 'Unknown Title';
|
|
19
|
+
let cover = data.thumbnail_url || '';
|
|
20
|
+
|
|
21
|
+
const searchQuery = title;
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
id: trackId,
|
|
25
|
+
title: title.replace(/\s*-\s*(Original Audio|Spotify|Audio|Official Video|Lyrics)$/i, '').trim(),
|
|
26
|
+
artist: 'Spotify Track',
|
|
27
|
+
cover: cover,
|
|
28
|
+
url: url,
|
|
29
|
+
searchQuery: searchQuery
|
|
30
|
+
};
|
|
31
|
+
} catch (e) {
|
|
32
|
+
throw new Error('Failed to fetch Spotify metadata: ' + e.message);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function spotifyDownloader(url, options = {}) {
|
|
37
|
+
const { format = 'mp3' } = options;
|
|
38
|
+
|
|
39
|
+
if (typeof url !== 'string' || !url.includes('spotify.com/')) {
|
|
40
|
+
return {
|
|
41
|
+
status: false,
|
|
42
|
+
platform: 'spotify',
|
|
43
|
+
message: 'Invalid Spotify URL. Only track links are supported.'
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const trackMetadata = await getSpotifyTrackInfo(url);
|
|
49
|
+
|
|
50
|
+
// search on youtube for the track
|
|
51
|
+
let videoUrl = null;
|
|
52
|
+
try {
|
|
53
|
+
const searchResult = await youtubedl(`ytsearch1:${trackMetadata.searchQuery}`, {
|
|
54
|
+
dumpSingleJson: true,
|
|
55
|
+
noCheckCertificates: true,
|
|
56
|
+
noWarnings: true,
|
|
57
|
+
});
|
|
58
|
+
videoUrl = searchResult.entries?.[0]?.webpage_url || null;
|
|
59
|
+
} catch (err) {
|
|
60
|
+
console.error('Spotify YouTube Search Error:', err.message);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
status: true,
|
|
65
|
+
platform: 'spotify',
|
|
66
|
+
data: {
|
|
67
|
+
id: trackMetadata.id,
|
|
68
|
+
title: trackMetadata.title,
|
|
69
|
+
artist: trackMetadata.artist,
|
|
70
|
+
cover: trackMetadata.cover,
|
|
71
|
+
url: trackMetadata.url,
|
|
72
|
+
videoUrl: videoUrl, // This will be used by test.js to auto-download
|
|
73
|
+
searchQuery: trackMetadata.searchQuery,
|
|
74
|
+
format: format,
|
|
75
|
+
isAudio: true,
|
|
76
|
+
message: videoUrl ? 'Successfully found a matching video on YouTube' : 'Could not find a matching video on YouTube'
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
} catch (err) {
|
|
81
|
+
return {
|
|
82
|
+
status: false,
|
|
83
|
+
platform: 'spotify',
|
|
84
|
+
message: err.message
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
module.exports = spotifyDownloader;
|
|
@@ -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;
|