@lmna22/aio-downloader 1.0.1
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 +416 -0
- package/package.json +51 -0
- package/src/download.js +53 -0
- package/src/index.js +62 -0
- package/src/lib/instagram.js +395 -0
- package/src/lib/pinterest.js +313 -0
- package/src/lib/pixiv.js +425 -0
- package/src/lib/tiktok.js +120 -0
- package/src/lib/twitter.js +239 -0
- package/src/lib/youtube.js +257 -0
- package/src/utils.js +93 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
const axios = require('axios');
|
|
2
|
+
const querystring = require('querystring');
|
|
3
|
+
|
|
4
|
+
const defaultFeatures = {
|
|
5
|
+
"creator_subscriptions_tweet_preview_api_enabled": true,
|
|
6
|
+
"premium_content_api_read_enabled": false,
|
|
7
|
+
"communities_web_enable_tweet_community_results_fetch": true,
|
|
8
|
+
"c9s_tweet_anatomy_moderator_badge_enabled": true,
|
|
9
|
+
"responsive_web_grok_analyze_button_fetch_trends_enabled": false,
|
|
10
|
+
"responsive_web_grok_analyze_post_followups_enabled": false,
|
|
11
|
+
"responsive_web_jetfuel_frame": false,
|
|
12
|
+
"responsive_web_grok_share_attachment_enabled": true,
|
|
13
|
+
"articles_preview_enabled": true,
|
|
14
|
+
"responsive_web_edit_tweet_api_enabled": true,
|
|
15
|
+
"graphql_is_translatable_rweb_tweet_is_translatable_enabled": true,
|
|
16
|
+
"view_counts_everywhere_api_enabled": true,
|
|
17
|
+
"longform_notetweets_consumption_enabled": true,
|
|
18
|
+
"responsive_web_twitter_article_tweet_consumption_enabled": true,
|
|
19
|
+
"tweet_awards_web_tipping_enabled": false,
|
|
20
|
+
"responsive_web_grok_show_grok_translated_post": false,
|
|
21
|
+
"responsive_web_grok_analysis_button_from_backend": false,
|
|
22
|
+
"creator_subscriptions_quote_tweet_preview_enabled": false,
|
|
23
|
+
"freedom_of_speech_not_reach_fetch_enabled": true,
|
|
24
|
+
"standardized_nudges_misinfo": true,
|
|
25
|
+
"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true,
|
|
26
|
+
"longform_notetweets_rich_text_read_enabled": true,
|
|
27
|
+
"longform_notetweets_inline_media_enabled": true,
|
|
28
|
+
"profile_label_improvements_pcf_label_in_post_enabled": true,
|
|
29
|
+
"rweb_tipjar_consumption_enabled": true,
|
|
30
|
+
"responsive_web_graphql_exclude_directive_enabled": true,
|
|
31
|
+
"verified_phone_label_enabled": false,
|
|
32
|
+
"responsive_web_grok_image_annotation_enabled": true,
|
|
33
|
+
"responsive_web_graphql_skip_user_profile_image_extensions_enabled": false,
|
|
34
|
+
"responsive_web_graphql_timeline_navigation_enabled": true,
|
|
35
|
+
"responsive_web_enhance_cards_enabled": false
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const defaultVariables = {
|
|
39
|
+
"withCommunity": false,
|
|
40
|
+
"includePromotedContent": false,
|
|
41
|
+
"withVoice": false
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
const USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:84.0) Gecko/20100101 Firefox/84.0';
|
|
45
|
+
|
|
46
|
+
const DEFAULT_HEADERS = {
|
|
47
|
+
'User-Agent': USER_AGENT,
|
|
48
|
+
'Accept': '*/*',
|
|
49
|
+
'Accept-Language': 'en-US, en, *;q=0.5',
|
|
50
|
+
'Accept-Encoding': 'gzip, deflate, br',
|
|
51
|
+
'TE': 'trailers',
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
const tokenCache = new Map();
|
|
55
|
+
|
|
56
|
+
async function getTokens(tweetUrl) {
|
|
57
|
+
if (tokenCache.has(tweetUrl)) {
|
|
58
|
+
return tokenCache.get(tweetUrl);
|
|
59
|
+
}
|
|
60
|
+
|
|
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
|
+
|
|
64
|
+
if (!mainJsUrlMatch) {
|
|
65
|
+
throw new Error('Failed to find main JS URL.');
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const mainJsUrl = mainJsUrlMatch[0];
|
|
69
|
+
const bearerToken = await extractBearerToken(mainJsUrl);
|
|
70
|
+
const guestToken = await getGuestToken(redirectPageData);
|
|
71
|
+
const tokens = { bearer: bearerToken, guest: guestToken };
|
|
72
|
+
tokenCache.set(tweetUrl, tokens);
|
|
73
|
+
return tokens;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async function extractBearerToken(mainJsUrl) {
|
|
77
|
+
const { data: mainJsData } = await axios.get(mainJsUrl);
|
|
78
|
+
const match = mainJsData.match(/:"Bearer ([^"]+)"/);
|
|
79
|
+
if (!match) throw new Error('Failed to find bearer token.');
|
|
80
|
+
return match[1];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async function getGuestToken(data) {
|
|
84
|
+
const match = data.match(/gt=(\d+)/);
|
|
85
|
+
if (!match) throw new Error('Failed to find guest token.');
|
|
86
|
+
return match[1];
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function extractTweetId(tweetUrl) {
|
|
90
|
+
const match = tweetUrl.match(/(?<=status\/)\d+/);
|
|
91
|
+
if (!match) throw new Error('Could not parse tweet id from URL. Make sure you are using the correct URL.');
|
|
92
|
+
return match[0];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function updateFeaturesAndVariables(errorData, features, variables) {
|
|
96
|
+
const neededVariablesPattern = /Variable '([^']+)'/;
|
|
97
|
+
const neededFeaturesPattern = /The following features cannot be null: ([^"]+)/;
|
|
98
|
+
|
|
99
|
+
if (!errorData || !errorData.errors || !Array.isArray(errorData.errors)) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
errorData.errors.forEach(error => {
|
|
104
|
+
if (!error || !error.message) return;
|
|
105
|
+
|
|
106
|
+
const neededVars = error.message.match(neededVariablesPattern);
|
|
107
|
+
if (neededVars) {
|
|
108
|
+
neededVars.slice(1).forEach(v => variables[v] = true);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const neededFeatures = error.message.match(neededFeaturesPattern);
|
|
112
|
+
if (neededFeatures) {
|
|
113
|
+
neededFeatures[1].split(',').forEach(feature => {
|
|
114
|
+
features[feature.trim()] = true;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function getDetailsUrl(tID, features, variables) {
|
|
121
|
+
const variablesCopy = { ...variables, tweetId: tID };
|
|
122
|
+
const variablesEncoded = querystring.escape(JSON.stringify(variablesCopy));
|
|
123
|
+
const featuresEncoded = querystring.escape(JSON.stringify(features));
|
|
124
|
+
return `https://api.x.com/graphql/Vg2Akr5FzUmF0sTplA5k6g/TweetResultByRestId?variables=${variablesEncoded}&features=${featuresEncoded}`;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function extractMp4s(data) {
|
|
128
|
+
if (!data || !data.core || !data.legacy || !data.views) {
|
|
129
|
+
return {
|
|
130
|
+
creator: '@fort.kun',
|
|
131
|
+
status: false,
|
|
132
|
+
message: 'Invalid data format from Twitter API'
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const { core, legacy, views } = data;
|
|
137
|
+
const author = core.user_results.result.legacy.screen_name;
|
|
138
|
+
let description = legacy.full_text.replace(/https:\/\/t\.co\/[a-zA-Z0-9_-]+\s*$/, '').trim();
|
|
139
|
+
const listUrl = legacy.entities?.media?.map(media => {
|
|
140
|
+
if (media.type === 'video' || media.type === 'animated_gif') {
|
|
141
|
+
const variants = media.video_info?.variants;
|
|
142
|
+
|
|
143
|
+
if (variants && variants.length > 0) {
|
|
144
|
+
const mp4Variants = variants.filter(v => v.bitrate !== undefined);
|
|
145
|
+
|
|
146
|
+
if (mp4Variants.length > 0) {
|
|
147
|
+
const bestQualityVariant = mp4Variants.reduce((prev, curr) => (
|
|
148
|
+
curr.bitrate > prev.bitrate ? curr : prev
|
|
149
|
+
), mp4Variants[0]);
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
type: media.type === 'video' ? 'video' : 'gif',
|
|
153
|
+
thumb: media.media_url_https,
|
|
154
|
+
url: bestQualityVariant.url
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
return null;
|
|
159
|
+
} else if (media.type === 'photo') {
|
|
160
|
+
return {
|
|
161
|
+
type: 'image',
|
|
162
|
+
url: `${media.media_url_https}?format=png&name=large`
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
return null;
|
|
166
|
+
}).filter(Boolean) || [];
|
|
167
|
+
|
|
168
|
+
return {
|
|
169
|
+
creator: '@fort.kun',
|
|
170
|
+
status: true,
|
|
171
|
+
data: {
|
|
172
|
+
author,
|
|
173
|
+
like: legacy.favorite_count,
|
|
174
|
+
view: views.count,
|
|
175
|
+
retweet: legacy.retweet_count,
|
|
176
|
+
description,
|
|
177
|
+
sensitiveContent: legacy.possibly_sensitive,
|
|
178
|
+
result: listUrl,
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function twitterDownloader(tweetUrl) {
|
|
184
|
+
let features = { ...defaultFeatures };
|
|
185
|
+
let variables = { ...defaultVariables };
|
|
186
|
+
|
|
187
|
+
try {
|
|
188
|
+
const token = await getTokens(tweetUrl);
|
|
189
|
+
if (!token.bearer || !token.guest) throw new Error('Failed to get bearer token.');
|
|
190
|
+
const { guest, bearer } = token;
|
|
191
|
+
const tweetId = extractTweetId(tweetUrl);
|
|
192
|
+
let url = getDetailsUrl(tweetId, features, variables);
|
|
193
|
+
let details;
|
|
194
|
+
|
|
195
|
+
for (let retries = 0; retries < 3; retries++) {
|
|
196
|
+
try {
|
|
197
|
+
const { data, status } = await axios.get(url, {
|
|
198
|
+
headers: {
|
|
199
|
+
"Authorization": `Bearer ${bearer}`,
|
|
200
|
+
"X-Guest-Token": guest,
|
|
201
|
+
},
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
if (status === 200 && data?.data?.tweetResult?.result) {
|
|
205
|
+
details = data.data.tweetResult.result;
|
|
206
|
+
break;
|
|
207
|
+
} else {
|
|
208
|
+
throw new Error(`Request failed with status ${status}`);
|
|
209
|
+
}
|
|
210
|
+
} catch (error) {
|
|
211
|
+
if (error.response?.status === 400 && error.response.data) {
|
|
212
|
+
updateFeaturesAndVariables(error.response.data, features, variables);
|
|
213
|
+
url = getDetailsUrl(tweetId, features, variables);
|
|
214
|
+
} else {
|
|
215
|
+
throw error;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (!details) {
|
|
221
|
+
return {
|
|
222
|
+
creator: '@fort.kun',
|
|
223
|
+
status: false,
|
|
224
|
+
message: 'Failed to retrieve tweet details after multiple retries.'
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return extractMp4s(details);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
tokenCache.delete(tweetUrl);
|
|
231
|
+
return {
|
|
232
|
+
creator: '@fort.kun',
|
|
233
|
+
status: false,
|
|
234
|
+
message: error.message || 'An unexpected error occurred'
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
module.exports = twitterDownloader;
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const fsPromises = require('fs').promises;
|
|
3
|
+
const { spawn } = require('child_process');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const os = require('os');
|
|
6
|
+
const { join } = require('path');
|
|
7
|
+
|
|
8
|
+
let ffmpegPath;
|
|
9
|
+
try {
|
|
10
|
+
ffmpegPath = require('ffmpeg-static');
|
|
11
|
+
} catch (err) { }
|
|
12
|
+
|
|
13
|
+
let youtubedl;
|
|
14
|
+
try {
|
|
15
|
+
youtubedl = require('youtube-dl-exec');
|
|
16
|
+
} catch (err) { }
|
|
17
|
+
|
|
18
|
+
const tempDir = os.tmpdir();
|
|
19
|
+
|
|
20
|
+
const QUALITY_MAP = {
|
|
21
|
+
1: '160',
|
|
22
|
+
2: '134',
|
|
23
|
+
3: '135',
|
|
24
|
+
4: '136',
|
|
25
|
+
5: '137',
|
|
26
|
+
6: '264',
|
|
27
|
+
7: '266',
|
|
28
|
+
8: 'bestaudio',
|
|
29
|
+
9: 'bitrateList'
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
async function youtubeDownloader(link, qualityIndex) {
|
|
33
|
+
if (!youtubedl) {
|
|
34
|
+
return {
|
|
35
|
+
status: false,
|
|
36
|
+
message: `youtube-dl-exec not found, can't download video`,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
try {
|
|
40
|
+
let quality = QUALITY_MAP[qualityIndex] || QUALITY_MAP[2];
|
|
41
|
+
|
|
42
|
+
const info = await youtubedl(link, {
|
|
43
|
+
dumpSingleJson: true,
|
|
44
|
+
noCheckCertificates: true,
|
|
45
|
+
noWarnings: true,
|
|
46
|
+
preferFreeFormats: true,
|
|
47
|
+
addHeader: ['referer:youtube.com', 'user-agent:googlebot']
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const videoDetails = info;
|
|
51
|
+
const thumb = info.thumbnail;
|
|
52
|
+
|
|
53
|
+
const tempInfoFile = path.join(tempDir, `info_${Date.now()}.json`);
|
|
54
|
+
await fsPromises.writeFile(tempInfoFile, JSON.stringify(info));
|
|
55
|
+
|
|
56
|
+
let result;
|
|
57
|
+
if (quality === 'bitrateList') {
|
|
58
|
+
result = getBitrateList(info);
|
|
59
|
+
} else if (qualityIndex > 7 || quality === 'bestaudio') {
|
|
60
|
+
result = await downloadAudioOnly(tempInfoFile, quality, videoDetails, thumb);
|
|
61
|
+
} else {
|
|
62
|
+
result = await downloadVideoWithAudio(tempInfoFile, quality, videoDetails, thumb);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
await fsPromises.unlink(tempInfoFile);
|
|
66
|
+
|
|
67
|
+
return result;
|
|
68
|
+
|
|
69
|
+
} catch (err) {
|
|
70
|
+
return {
|
|
71
|
+
creator: '@fort.kun',
|
|
72
|
+
status: false,
|
|
73
|
+
message: err.message,
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getBitrateList(info) {
|
|
79
|
+
const bitrateList = info.formats
|
|
80
|
+
.filter(element => element.acodec !== 'none' && element.vcodec === 'none')
|
|
81
|
+
.map(element => ({
|
|
82
|
+
codec: element.acodec,
|
|
83
|
+
bitrate: element.abr,
|
|
84
|
+
format_id: element.format_id
|
|
85
|
+
}))
|
|
86
|
+
.sort((a, b) => b.bitrate - a.bitrate);
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
creator: '@fort.kun',
|
|
90
|
+
status: true,
|
|
91
|
+
data: { bitrateList }
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function downloadAudioOnly(infoFile, quality, videoDetails, thumb) {
|
|
96
|
+
const tempMp3 = path.join(tempDir, `temp_audio_${Date.now()}.mp3`);
|
|
97
|
+
|
|
98
|
+
await youtubedl.exec('', {
|
|
99
|
+
loadInfoJson: infoFile,
|
|
100
|
+
extractAudio: true,
|
|
101
|
+
audioFormat: 'mp3',
|
|
102
|
+
audioQuality: '0',
|
|
103
|
+
output: tempMp3
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const mp3Buffer = await fsPromises.readFile(tempMp3);
|
|
107
|
+
await fsPromises.unlink(tempMp3);
|
|
108
|
+
|
|
109
|
+
return createResponse(videoDetails, mp3Buffer, quality, thumb, 'mp3');
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function downloadVideoWithAudio(infoFile, quality, videoDetails, thumb) {
|
|
113
|
+
const baseName = `temp_video_${Date.now()}`;
|
|
114
|
+
const videoOutput = path.join(tempDir, `${baseName}.fvideo.mp4`);
|
|
115
|
+
const audioOutput = path.join(tempDir, `${baseName}.faudio.m4a`);
|
|
116
|
+
const finalOutput = path.join(tempDir, `${baseName}.mp4`);
|
|
117
|
+
|
|
118
|
+
try {
|
|
119
|
+
await youtubedl.exec('', {
|
|
120
|
+
loadInfoJson: infoFile,
|
|
121
|
+
format: quality,
|
|
122
|
+
output: videoOutput
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
await youtubedl.exec('', {
|
|
126
|
+
loadInfoJson: infoFile,
|
|
127
|
+
format: 'bestaudio',
|
|
128
|
+
output: audioOutput
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
await new Promise((resolve, reject) => {
|
|
132
|
+
const ffmpeg = spawn(ffmpegPath, [
|
|
133
|
+
'-i', videoOutput,
|
|
134
|
+
'-i', audioOutput,
|
|
135
|
+
'-c:v', 'copy',
|
|
136
|
+
'-c:a', 'aac',
|
|
137
|
+
'-strict', 'experimental',
|
|
138
|
+
finalOutput
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
ffmpeg.on('close', (code) => {
|
|
142
|
+
if (code === 0) {
|
|
143
|
+
resolve();
|
|
144
|
+
} else {
|
|
145
|
+
reject(new Error(`ffmpeg exited with code ${code}`));
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
const mp4Buffer = await fsPromises.readFile(finalOutput);
|
|
151
|
+
|
|
152
|
+
await fsPromises.unlink(videoOutput);
|
|
153
|
+
await fsPromises.unlink(audioOutput);
|
|
154
|
+
await fsPromises.unlink(finalOutput);
|
|
155
|
+
|
|
156
|
+
return createResponse(videoDetails, mp4Buffer, quality, thumb, 'mp4');
|
|
157
|
+
|
|
158
|
+
} catch (err) {
|
|
159
|
+
throw err;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function createResponse(videoDetails, buffer, quality, thumb, type) {
|
|
164
|
+
return {
|
|
165
|
+
creator: '@fort.kun',
|
|
166
|
+
status: true,
|
|
167
|
+
data: {
|
|
168
|
+
title: videoDetails.title,
|
|
169
|
+
result: buffer,
|
|
170
|
+
size: buffer.length,
|
|
171
|
+
quality,
|
|
172
|
+
desc: videoDetails.description,
|
|
173
|
+
views: videoDetails.view_count,
|
|
174
|
+
likes: videoDetails.like_count,
|
|
175
|
+
dislikes: 0,
|
|
176
|
+
channel: videoDetails.uploader,
|
|
177
|
+
uploadDate: videoDetails.upload_date,
|
|
178
|
+
thumb,
|
|
179
|
+
type
|
|
180
|
+
},
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
function sanitizeTitle(title) {
|
|
185
|
+
return title
|
|
186
|
+
.replace(/[\/\\:*?"<>|]/g, '_')
|
|
187
|
+
.trim();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
async function youtubePlaylistDownloader(url, quality, folderPath = join(process.cwd() + '/temp')) {
|
|
191
|
+
let playlistId;
|
|
192
|
+
try {
|
|
193
|
+
playlistId = url.slice(url.indexOf("list="), url.indexOf("&index"));
|
|
194
|
+
} catch {
|
|
195
|
+
return {
|
|
196
|
+
creator: '@fort.kun',
|
|
197
|
+
status: false,
|
|
198
|
+
message: 'Invalid Playlist URL'
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
try {
|
|
202
|
+
const axios = require('axios');
|
|
203
|
+
const { data } = await axios.get(url);
|
|
204
|
+
const htmlStr = data;
|
|
205
|
+
|
|
206
|
+
let arr = htmlStr.split('"watchEndpoint":{"videoId":"');
|
|
207
|
+
var db = {};
|
|
208
|
+
|
|
209
|
+
for (var i = 1; i < arr.length; i++) {
|
|
210
|
+
let str = arr[i];
|
|
211
|
+
let eI = str.indexOf('"');
|
|
212
|
+
if (str.slice(eI, eI + 13) != '","playlistId') continue;
|
|
213
|
+
let sstr = str.slice(0, eI);
|
|
214
|
+
db[sstr] = 1;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
let title = htmlStr.match(/property="og:title" content="(.+?)"/)?.[1];
|
|
218
|
+
|
|
219
|
+
let resultPath = [];
|
|
220
|
+
let metadata = [];
|
|
221
|
+
|
|
222
|
+
if (!fs.existsSync(folderPath)) {
|
|
223
|
+
fs.mkdirSync(folderPath);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
for (const key of Object.keys(db)) {
|
|
227
|
+
const res = await youtubeDownloader(`https://www.youtube.com/watch?v=${key}`, quality);
|
|
228
|
+
if (res.status && res.data) {
|
|
229
|
+
const filePath = join(folderPath, `${sanitizeTitle(res.data.title)}.${res.data.type}`);
|
|
230
|
+
fs.writeFileSync(filePath, res.data.result);
|
|
231
|
+
resultPath.push(filePath);
|
|
232
|
+
metadata.push(res.data);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
creator: '@fort.kun',
|
|
238
|
+
status: true,
|
|
239
|
+
data: {
|
|
240
|
+
title,
|
|
241
|
+
resultPath,
|
|
242
|
+
metadata
|
|
243
|
+
}
|
|
244
|
+
};
|
|
245
|
+
} catch (e) {
|
|
246
|
+
return {
|
|
247
|
+
creator: '@fort.kun',
|
|
248
|
+
status: false,
|
|
249
|
+
message: e.message || 'Something went wrong.'
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = {
|
|
255
|
+
youtubeDownloader,
|
|
256
|
+
youtubePlaylistDownloader
|
|
257
|
+
};
|
package/src/utils.js
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const axios = require("axios");
|
|
2
|
+
const path = require("path");
|
|
3
|
+
|
|
4
|
+
const DEFAULT_UA = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36";
|
|
5
|
+
|
|
6
|
+
function delay(ms) {
|
|
7
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function cleanText(text) {
|
|
11
|
+
if (!text) return null;
|
|
12
|
+
const t = String(text).replace(/\s+/g, " ").trim();
|
|
13
|
+
return t || null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function sanitizeFileName(name) {
|
|
17
|
+
return String(name || "untitled")
|
|
18
|
+
.replace(/[<>:"/\\|?*\x00-\x1F]/g, "_")
|
|
19
|
+
.replace(/\s+/g, " ")
|
|
20
|
+
.trim()
|
|
21
|
+
.slice(0, 120);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function getExtFromUrl(url, fallback = ".mp4") {
|
|
25
|
+
try {
|
|
26
|
+
const pathname = new URL(url).pathname;
|
|
27
|
+
const ext = path.extname(pathname);
|
|
28
|
+
if (ext) return ext.split("?")[0];
|
|
29
|
+
return fallback;
|
|
30
|
+
} catch {
|
|
31
|
+
return fallback;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function formatNumber(num) {
|
|
36
|
+
return new Intl.NumberFormat("en-US").format(num || 0);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function isValidUrl(url) {
|
|
40
|
+
if (!url) return false;
|
|
41
|
+
return /^https?:\/\//i.test(url);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function safeJsonParse(text) {
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(text);
|
|
47
|
+
} catch {
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function uniqBy(arr, keyFn) {
|
|
53
|
+
const seen = new Set();
|
|
54
|
+
const out = [];
|
|
55
|
+
for (const item of arr) {
|
|
56
|
+
const key = keyFn(item);
|
|
57
|
+
if (!key || seen.has(key)) continue;
|
|
58
|
+
seen.add(key);
|
|
59
|
+
out.push(item);
|
|
60
|
+
}
|
|
61
|
+
return out;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function makeRequest(url, options = {}) {
|
|
65
|
+
const config = {
|
|
66
|
+
headers: {
|
|
67
|
+
"User-Agent": DEFAULT_UA,
|
|
68
|
+
...options.headers,
|
|
69
|
+
},
|
|
70
|
+
timeout: options.timeout || 30000,
|
|
71
|
+
maxRedirects: options.maxRedirects || 5,
|
|
72
|
+
...options,
|
|
73
|
+
};
|
|
74
|
+
delete config.headers;
|
|
75
|
+
config.headers = {
|
|
76
|
+
"User-Agent": DEFAULT_UA,
|
|
77
|
+
...options.headers,
|
|
78
|
+
};
|
|
79
|
+
return axios(url, config);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = {
|
|
83
|
+
DEFAULT_UA,
|
|
84
|
+
delay,
|
|
85
|
+
cleanText,
|
|
86
|
+
sanitizeFileName,
|
|
87
|
+
getExtFromUrl,
|
|
88
|
+
formatNumber,
|
|
89
|
+
isValidUrl,
|
|
90
|
+
safeJsonParse,
|
|
91
|
+
uniqBy,
|
|
92
|
+
makeRequest,
|
|
93
|
+
};
|