@soyaxell09/zenbot-scraper 1.0.2 → 1.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@soyaxell09/zenbot-scraper",
3
- "version": "1.0.2",
3
+ "version": "1.0.3",
4
4
  "description": "Scrapers de descarga y búsqueda para bots de WhatsApp — YouTube, TikTok, Facebook, Twitter, Pinterest, MediaFire, GitHub, APK y más.",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -1,249 +0,0 @@
1
- /*
2
- * © Created by AxelDev09 🔥
3
- * GitHub: https://github.com/AxelDev09
4
- * Instagram: @axeldev09
5
- * Deja los créditos we 🗣️
6
- */
7
-
8
- import axios from 'axios'
9
-
10
- const INNERTUBE_URL = 'https://www.youtube.com/youtubei/v1'
11
- let _config = null
12
-
13
- const delay = ms => new Promise(r => setTimeout(r, ms))
14
-
15
- function parseFileSize(size) {
16
- if (!size) return 0
17
- const units = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3, TB: 1024 ** 4 }
18
- const match = size.toString().trim().match(/([\d.]+)\s*(B|KB|MB|GB|TB)/i)
19
- if (!match) return 0
20
- return Math.round(parseFloat(match[1]) * (units[match[2].toUpperCase()] || 1))
21
- }
22
-
23
- function formatFileSize(bytes) {
24
- if (!bytes || isNaN(bytes)) return '0 B'
25
- const units = ['B', 'KB', 'MB', 'GB', 'TB']
26
- let i = 0
27
- while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++ }
28
- const value = bytes.toFixed(1).replace(/\.0$/, '')
29
- return `${value} ${units[i]}`
30
- }
31
-
32
- export async function getFileSize(url) {
33
- try {
34
- const res = await axios.head(url, { timeout: 10000 })
35
- const bytes = parseInt(res.headers['content-length'] || 0)
36
- return formatFileSize(bytes)
37
- } catch { return '0 B' }
38
- }
39
-
40
- function normalizeYT(url) {
41
- try {
42
- const u = new URL(url)
43
- if (u.hostname.includes('youtu.be')) return url
44
- if (u.hostname.includes('youtube.com')) {
45
- if (u.pathname.includes('/watch')) return `https://youtu.be/${u.searchParams.get('v')}`
46
- if (u.pathname.includes('/shorts/')) return `https://youtu.be/${u.pathname.split('/shorts/')[1]}`
47
- if (u.pathname.includes('/embed/')) return `https://youtu.be/${u.pathname.split('/embed/')[1]}`
48
- }
49
- return url
50
- } catch { return url }
51
- }
52
-
53
- function parseVideoId(url) {
54
- const patterns = [
55
- /(?:v=|youtu\.be\/|shorts\/)([a-zA-Z0-9_-]{11})/,
56
- /^([a-zA-Z0-9_-]{11})$/,
57
- ]
58
- for (const p of patterns) {
59
- const m = url.match(p)
60
- if (m) return m[1]
61
- }
62
- return null
63
- }
64
-
65
- async function getConfig() {
66
- if (_config) return _config
67
- const res = await axios.get('https://www.youtube.com/', {
68
- headers: {
69
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
70
- 'Accept-Language': 'en-US,en;q=0.9',
71
- 'Accept': 'text/html,application/xhtml+xml',
72
- },
73
- timeout: 15000,
74
- })
75
- const html = res.data
76
- const key = html.match(/"INNERTUBE_API_KEY"\s*:\s*"([^"]+)"/)?.[1] || 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM394'
77
- const visitorData = html.match(/"visitorData"\s*:\s*"([^"]+)"/)?.[1] || ''
78
- const clientVersion = html.match(/"clientVersion"\s*:\s*"([^"]+)"/)?.[1] || '2.20240101.00.00'
79
- _config = { key, visitorData, clientVersion }
80
- return _config
81
- }
82
-
83
- async function waitForDownload(mediaUrl) {
84
- for (let i = 0; i < 15; i++) {
85
- try {
86
- const { data } = await axios.get(mediaUrl, { timeout: 15000 })
87
- if (data?.percent === 'Completed' && data?.fileUrl && data.fileUrl !== 'In Processing...')
88
- return data.fileUrl
89
- } catch {}
90
- await delay(4000)
91
- }
92
- throw new Error('No se pudo generar el enlace de descarga')
93
- }
94
-
95
- async function fetchYtdownto(url) {
96
- const { data } = await axios.post(
97
- 'https://app.ytdown.to/proxy.php',
98
- new URLSearchParams({ url }).toString(),
99
- {
100
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
101
- timeout: 20000,
102
- }
103
- )
104
- const api = data?.api
105
- if (!api) throw new Error('No se pudo obtener información del video')
106
- if (api.status === 'ERROR') throw new Error(api.message)
107
-
108
- const qualities = (api.mediaItems || []).map((v, i) => {
109
- const match = v?.mediaUrl?.match(/(\d+)p|(\d+)k/)
110
- const res = match ? match[0] : v.mediaQuality
111
- return {
112
- id: i + 1,
113
- type: v.type,
114
- quality: res,
115
- label: `${v.mediaExtension?.toUpperCase()} - ${v.mediaQuality}`,
116
- size: v.mediaFileSize,
117
- sizeB: parseFileSize(v.mediaFileSize),
118
- mediaUrl: v.mediaUrl,
119
- duration: v.mediaDuration,
120
- }
121
- })
122
-
123
- return { api, qualities }
124
- }
125
-
126
- export async function ytDownload(input, type = 'video', quality = '360p') {
127
- let url = input
128
-
129
- if (!parseVideoId(input)) {
130
- const results = await ytSearch(input, 3)
131
- if (!results.length) throw new Error('No se encontró ningún video para: ' + input)
132
- for (const r of results) {
133
- try { return await ytDownload(r.url, type, quality) } catch {}
134
- }
135
- throw new Error('No se pudo descargar ningún resultado para: ' + input)
136
- }
137
-
138
- url = normalizeYT(input)
139
-
140
- const { api, qualities } = await fetchYtdownto(url)
141
-
142
- const isAudio = type === 'mp3' || type === 'audio'
143
- const targetQ = quality.toLowerCase()
144
-
145
- const filtered = isAudio
146
- ? qualities.filter(v => v.type === 'audio' || v.quality?.includes('k'))
147
- : qualities.filter(v => v.type === 'video' || v.quality?.includes('p'))
148
-
149
- const selected = filtered.find(v => v.quality?.toLowerCase() === targetQ)
150
- || filtered[0]
151
-
152
- if (!selected) {
153
- const disponibles = qualities.map(v => v.quality).filter(Boolean).join(', ')
154
- throw new Error(`Calidad ${quality} no disponible. Disponibles: ${disponibles}`)
155
- }
156
-
157
- const dlUrl = await waitForDownload(selected.mediaUrl)
158
-
159
- return {
160
- title: api.title,
161
- uploader: api.userInfo?.name || '',
162
- views: api.mediaStats?.viewsCount || '',
163
- thumb: api.imagePreviewUrl || '',
164
- type: isAudio ? 'audio' : 'video',
165
- quality: selected.quality,
166
- size: selected.size,
167
- sizeB: selected.sizeB,
168
- duration: selected.duration,
169
- url: dlUrl,
170
- }
171
- }
172
-
173
- export async function ytInfo(input) {
174
- let url = input
175
-
176
- if (!parseVideoId(input)) {
177
- const results = await ytSearch(input, 1)
178
- if (!results.length) throw new Error('No se encontró ningún video para: ' + input)
179
- url = results[0].url
180
- }
181
-
182
- url = normalizeYT(url)
183
- const { api, qualities } = await fetchYtdownto(url)
184
-
185
- return {
186
- title: api.title,
187
- uploader: api.userInfo?.name || '',
188
- views: api.mediaStats?.viewsCount || '',
189
- thumb: api.imagePreviewUrl || '',
190
- qualities,
191
- }
192
- }
193
-
194
- export async function ytSearch(query, limit = 5) {
195
- const cfg = await getConfig()
196
- const res = await axios.post(
197
- `${INNERTUBE_URL}/search?key=${cfg.key}&prettyPrint=false`,
198
- {
199
- query,
200
- context: {
201
- client: {
202
- clientName: 'WEB',
203
- clientVersion: cfg.clientVersion,
204
- hl: 'en', gl: 'US',
205
- visitorData: cfg.visitorData,
206
- },
207
- },
208
- },
209
- {
210
- headers: {
211
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
212
- 'X-YouTube-Client-Name': '1',
213
- 'X-YouTube-Client-Version': cfg.clientVersion,
214
- 'Content-Type': 'application/json',
215
- 'X-Goog-Visitor-Id': cfg.visitorData,
216
- },
217
- timeout: 15000,
218
- }
219
- )
220
-
221
- const contents =
222
- res.data?.contents
223
- ?.twoColumnSearchResultsRenderer
224
- ?.primaryContents
225
- ?.sectionListRenderer
226
- ?.contents?.[0]
227
- ?.itemSectionRenderer
228
- ?.contents || []
229
-
230
- const results = []
231
- for (const item of contents) {
232
- if (results.length >= limit) break
233
- const v = item?.videoRenderer
234
- if (!v?.videoId) continue
235
- results.push({
236
- id: v.videoId,
237
- title: v.title?.runs?.[0]?.text || '',
238
- url: `https://www.youtube.com/watch?v=${v.videoId}`,
239
- thumbnail: v.thumbnail?.thumbnails?.at(-1)?.url || '',
240
- duration: v.lengthText?.simpleText || '',
241
- views: v.viewCountText?.simpleText || '',
242
- channel: v.ownerText?.runs?.[0]?.text || '',
243
- published: v.publishedTimeText?.simpleText || '',
244
- })
245
- }
246
-
247
- if (!results.length) throw new Error('Sin resultados')
248
- return results
249
- }