@soyaxell09/zenbot-scraper 1.0.0 → 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 +1 -1
- package/src/tools/youtube.js +0 -249
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@soyaxell09/zenbot-scraper",
|
|
3
|
-
"version": "1.0.
|
|
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",
|
package/src/tools/youtube.js
DELETED
|
@@ -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
|
-
}
|