@soyaxell09/zenbot-scraper 1.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.
@@ -0,0 +1,190 @@
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
+ import ytdl from '@distube/ytdl-core'
10
+
11
+ const INNERTUBE_URL = 'https://www.youtube.com/youtubei/v1'
12
+ let _config = null
13
+
14
+ async function getConfig() {
15
+ if (_config) return _config
16
+ const res = await axios.get('https://www.youtube.com/', {
17
+ headers: {
18
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
19
+ 'Accept-Language': 'en-US,en;q=0.9',
20
+ 'Accept': 'text/html,application/xhtml+xml',
21
+ },
22
+ timeout: 15000,
23
+ })
24
+ const html = res.data
25
+ const key = html.match(/"INNERTUBE_API_KEY"\s*:\s*"([^"]+)"/)?.[1] || 'AIzaSyA8eiZmM1FaDVjRy-df2KTyQ_vz_yYM394'
26
+ const visitorData = html.match(/"visitorData"\s*:\s*"([^"]+)"/)?.[1] || ''
27
+ const clientVersion = html.match(/"clientVersion"\s*:\s*"([^"]+)"/)?.[1] || '2.20240101.00.00'
28
+ _config = { key, visitorData, clientVersion }
29
+ return _config
30
+ }
31
+
32
+ function parseVideoId(url) {
33
+ const patterns = [
34
+ /(?:v=|youtu\.be\/|shorts\/)([a-zA-Z0-9_-]{11})/,
35
+ /^([a-zA-Z0-9_-]{11})$/,
36
+ ]
37
+ for (const p of patterns) {
38
+ const m = url.match(p)
39
+ if (m) return m[1]
40
+ }
41
+ return null
42
+ }
43
+
44
+ export async function ytInfo(url) {
45
+ const id = parseVideoId(url)
46
+ if (!id) throw new Error('URL/ID de YouTube inválido')
47
+
48
+ const info = await ytdl.getInfo(`https://www.youtube.com/watch?v=${id}`)
49
+ const vd = info.videoDetails
50
+ const formats = info.formats
51
+ .filter(f => f.url)
52
+ .map(f => ({
53
+ itag: f.itag,
54
+ url: f.url,
55
+ mimeType: f.mimeType || '',
56
+ quality: f.qualityLabel || f.quality || '',
57
+ bitrate: f.bitrate || f.averageBitrate || 0,
58
+ width: f.width || 0,
59
+ height: f.height || 0,
60
+ fps: f.fps || 0,
61
+ hasVideo: !!f.hasVideo,
62
+ hasAudio: !!f.hasAudio,
63
+ container: f.container || '',
64
+ codecs: f.codecs || '',
65
+ }))
66
+
67
+ return {
68
+ id,
69
+ title: vd.title || '',
70
+ author: vd.author?.name || vd.ownerChannelName || '',
71
+ channelId: vd.channelId || '',
72
+ duration: Number(vd.lengthSeconds || 0),
73
+ views: Number(vd.viewCount || 0),
74
+ thumbnail: vd.thumbnails?.at(-1)?.url || '',
75
+ description: vd.description?.slice(0, 300) || '',
76
+ isLive: vd.isLiveContent || false,
77
+ formats,
78
+ }
79
+ }
80
+
81
+ export async function ytDownload(url, type = 'video', quality = '360') {
82
+ const id = parseVideoId(url)
83
+ if (!id) throw new Error('URL de YouTube inválida')
84
+
85
+ const info = await ytInfo(id)
86
+ const formats = info.formats
87
+
88
+ if (type === 'mp3' || type === 'audio') {
89
+ const audioFormats = formats
90
+ .filter(f => f.hasAudio && !f.hasVideo)
91
+ .sort((a, b) => b.bitrate - a.bitrate)
92
+ if (!audioFormats.length) throw new Error('Sin formatos de audio disponibles')
93
+ const best = audioFormats[0]
94
+ return {
95
+ type: 'audio',
96
+ url: best.url,
97
+ mimeType: best.mimeType,
98
+ bitrate: best.bitrate,
99
+ duration: info.duration,
100
+ title: info.title,
101
+ author: info.author,
102
+ thumbnail: info.thumbnail,
103
+ }
104
+ }
105
+
106
+ const q = parseInt(quality)
107
+ const videoFormats = formats
108
+ .filter(f => f.hasVideo && f.hasAudio)
109
+ .sort((a, b) => b.height - a.height)
110
+
111
+ if (!videoFormats.length) {
112
+ const videoOnly = formats
113
+ .filter(f => f.hasVideo && !f.hasAudio)
114
+ .sort((a, b) => b.height - a.height)
115
+ if (!videoOnly.length) throw new Error('Sin formatos de video disponibles')
116
+ const best = videoOnly.find(f => f.height <= q) || videoOnly[0]
117
+ return {
118
+ type: 'video', url: best.url, mimeType: best.mimeType,
119
+ quality: best.quality, width: best.width, height: best.height,
120
+ fps: best.fps, title: info.title, author: info.author,
121
+ thumbnail: info.thumbnail, duration: info.duration,
122
+ note: 'solo_video_sin_audio',
123
+ }
124
+ }
125
+
126
+ const best = videoFormats.find(f => f.height <= q) || videoFormats[0]
127
+ return {
128
+ type: 'video', url: best.url, mimeType: best.mimeType,
129
+ quality: best.quality, width: best.width, height: best.height,
130
+ fps: best.fps, title: info.title, author: info.author,
131
+ thumbnail: info.thumbnail, duration: info.duration,
132
+ }
133
+ }
134
+
135
+ export async function ytSearch(query, limit = 5) {
136
+ const cfg = await getConfig()
137
+ const res = await axios.post(
138
+ `${INNERTUBE_URL}/search?key=${cfg.key}&prettyPrint=false`,
139
+ {
140
+ query,
141
+ context: {
142
+ client: {
143
+ clientName: 'WEB',
144
+ clientVersion: cfg.clientVersion,
145
+ hl: 'en', gl: 'US',
146
+ visitorData: cfg.visitorData,
147
+ },
148
+ },
149
+ },
150
+ {
151
+ headers: {
152
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
153
+ 'X-YouTube-Client-Name': '1',
154
+ 'X-YouTube-Client-Version': cfg.clientVersion,
155
+ 'Content-Type': 'application/json',
156
+ 'X-Goog-Visitor-Id': cfg.visitorData,
157
+ },
158
+ timeout: 15000,
159
+ }
160
+ )
161
+
162
+ const contents =
163
+ res.data?.contents
164
+ ?.twoColumnSearchResultsRenderer
165
+ ?.primaryContents
166
+ ?.sectionListRenderer
167
+ ?.contents?.[0]
168
+ ?.itemSectionRenderer
169
+ ?.contents || []
170
+
171
+ const results = []
172
+ for (const item of contents) {
173
+ if (results.length >= limit) break
174
+ const v = item?.videoRenderer
175
+ if (!v?.videoId) continue
176
+ results.push({
177
+ id: v.videoId,
178
+ title: v.title?.runs?.[0]?.text || '',
179
+ url: `https://www.youtube.com/watch?v=${v.videoId}`,
180
+ thumbnail: v.thumbnail?.thumbnails?.at(-1)?.url || '',
181
+ duration: v.lengthText?.simpleText || '',
182
+ views: v.viewCountText?.simpleText || '',
183
+ channel: v.ownerText?.runs?.[0]?.text || '',
184
+ published: v.publishedTimeText?.simpleText || '',
185
+ })
186
+ }
187
+
188
+ if (!results.length) throw new Error('Sin resultados')
189
+ return results
190
+ }
@@ -0,0 +1,131 @@
1
+ // Créditos a FG-error
2
+ import axios from 'axios'
3
+
4
+ const delay = ms => new Promise(r => setTimeout(r, ms))
5
+
6
+ function parseFileSize(size) {
7
+ if (!size) return 0
8
+ const units = { B: 1, KB: 1024, MB: 1024 ** 2, GB: 1024 ** 3, TB: 1024 ** 4 }
9
+ const match = size.toString().trim().match(/([\d.]+)\s*(B|KB|MB|GB|TB)/i)
10
+ if (!match) return 0
11
+ return Math.round(parseFloat(match[1]) * (units[match[2].toUpperCase()] || 1))
12
+ }
13
+
14
+ function formatFileSize(bytes) {
15
+ if (!bytes || isNaN(bytes)) return '0 B'
16
+ const units = ['B', 'KB', 'MB', 'GB', 'TB']
17
+ let i = 0
18
+ while (bytes >= 1024 && i < units.length - 1) { bytes /= 1024; i++ }
19
+ return `${bytes.toFixed(1).replace(/\.0$/, '')} ${units[i]}`
20
+ }
21
+
22
+ export async function getFileSizeV2(url) {
23
+ try {
24
+ const res = await axios.head(url, { timeout: 10000 })
25
+ const bytes = parseInt(res.headers['content-length'] || 0)
26
+ return formatFileSize(bytes)
27
+ } catch { return '0 B' }
28
+ }
29
+
30
+ function normalizeYT(url) {
31
+ try {
32
+ const u = new URL(url)
33
+ if (u.hostname.includes('youtu.be')) return url
34
+ if (u.hostname.includes('youtube.com')) {
35
+ if (u.pathname.includes('/watch')) return `https://youtu.be/${u.searchParams.get('v')}`
36
+ if (u.pathname.includes('/shorts/')) return `https://youtu.be/${u.pathname.split('/shorts/')[1]}`
37
+ if (u.pathname.includes('/embed/')) return `https://youtu.be/${u.pathname.split('/embed/')[1]}`
38
+ }
39
+ return url
40
+ } catch { return url }
41
+ }
42
+
43
+ async function waitForDownload(mediaUrl) {
44
+ for (let i = 0; i < 15; i++) {
45
+ try {
46
+ const { data } = await axios.get(mediaUrl, { timeout: 15000 })
47
+ if (data?.percent === 'Completed' && data?.fileUrl && data.fileUrl !== 'In Processing...')
48
+ return data.fileUrl
49
+ } catch {}
50
+ await delay(4000)
51
+ }
52
+ throw new Error('No se pudo generar el enlace de descarga')
53
+ }
54
+
55
+ async function fetchYtdownto(url) {
56
+ const { data } = await axios.post(
57
+ 'https://app.ytdown.to/proxy.php',
58
+ new URLSearchParams({ url }).toString(),
59
+ {
60
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
61
+ timeout: 20000,
62
+ }
63
+ )
64
+ const api = data?.api
65
+ if (!api) throw new Error('No se pudo obtener información del video')
66
+ if (api.status === 'ERROR') throw new Error(api.message)
67
+
68
+ const qualities = (api.mediaItems || []).map((v, i) => {
69
+ const match = v?.mediaUrl?.match(/(\d+)p|(\d+)k/)
70
+ const res = match ? match[0] : v.mediaQuality
71
+ return {
72
+ id: i + 1,
73
+ type: v.type,
74
+ quality: res,
75
+ label: `${v.mediaExtension?.toUpperCase()} - ${v.mediaQuality}`,
76
+ size: v.mediaFileSize,
77
+ sizeB: parseFileSize(v.mediaFileSize),
78
+ mediaUrl: v.mediaUrl,
79
+ duration: v.mediaDuration,
80
+ }
81
+ })
82
+
83
+ return { api, qualities }
84
+ }
85
+
86
+ export async function ytDownloadV2(url, type = 'video', quality = '360p') {
87
+ url = normalizeYT(url)
88
+
89
+ const { api, qualities } = await fetchYtdownto(url)
90
+
91
+ const isAudio = type === 'mp3' || type === 'audio'
92
+ const targetQ = quality.toLowerCase()
93
+
94
+ const filtered = isAudio
95
+ ? qualities.filter(v => v.type === 'audio' || v.quality?.includes('k'))
96
+ : qualities.filter(v => v.type === 'video' || v.quality?.includes('p'))
97
+
98
+ const selected = filtered.find(v => v.quality?.toLowerCase() === targetQ) || filtered[0]
99
+
100
+ if (!selected) {
101
+ const disponibles = qualities.map(v => v.quality).filter(Boolean).join(', ')
102
+ throw new Error(`Calidad ${quality} no disponible. Disponibles: ${disponibles}`)
103
+ }
104
+
105
+ const dlUrl = await waitForDownload(selected.mediaUrl)
106
+
107
+ return {
108
+ title: api.title,
109
+ uploader: api.userInfo?.name || '',
110
+ views: api.mediaStats?.viewsCount || '',
111
+ thumb: api.imagePreviewUrl || '',
112
+ type: isAudio ? 'audio' : 'video',
113
+ quality: selected.quality,
114
+ size: selected.size,
115
+ sizeB: selected.sizeB,
116
+ duration: selected.duration,
117
+ url: dlUrl,
118
+ }
119
+ }
120
+
121
+ export async function ytInfoV2(url) {
122
+ url = normalizeYT(url)
123
+ const { api, qualities } = await fetchYtdownto(url)
124
+ return {
125
+ title: api.title,
126
+ uploader: api.userInfo?.name || '',
127
+ views: api.mediaStats?.viewsCount || '',
128
+ thumb: api.imagePreviewUrl || '',
129
+ qualities,
130
+ }
131
+ }
@@ -0,0 +1,36 @@
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 HEADERS = {
11
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
12
+ }
13
+
14
+ const TENOR_KEY = 'AIzaSyAyimkuYQYF_FXVALexPuGQctUWRURdCYQ'
15
+
16
+ export async function giphy(query, limit = 5, type = 'search') {
17
+ const endpoint = type === 'trending'
18
+ ? `https://tenor.googleapis.com/v2/featured?key=${TENOR_KEY}&limit=${limit}&media_filter=gif`
19
+ : `https://tenor.googleapis.com/v2/search?q=${encodeURIComponent(query)}&key=${TENOR_KEY}&limit=${limit}&media_filter=gif`
20
+
21
+ const res = await axios.get(endpoint, { headers: HEADERS, timeout: 15000 })
22
+ const data = res.data?.results || []
23
+
24
+ if (!data.length) throw new Error('Sin resultados en Tenor')
25
+
26
+ return data.map(g => ({
27
+ id: g.id,
28
+ title: g.title || g.content_description || '',
29
+ url: g.itemurl || '',
30
+ gif: g.media_formats?.gif?.url || g.media_formats?.mediumgif?.url || '',
31
+ preview: g.media_formats?.nanogif?.url || g.media_formats?.tinygif?.url || '',
32
+ mp4: g.media_formats?.mp4?.url || g.media_formats?.loopedmp4?.url || '',
33
+ width: g.media_formats?.gif?.dims?.[0] || 0,
34
+ height: g.media_formats?.gif?.dims?.[1] || 0,
35
+ }))
36
+ }
@@ -0,0 +1,55 @@
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 HEADERS = {
11
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
12
+ }
13
+
14
+ export async function googleSearch(query, limit = 5) {
15
+ const res = await axios.get(
16
+ `https://api.duckduckgo.com/?q=${encodeURIComponent(query)}&format=json&no_html=1&skip_disambig=1`,
17
+ { headers: HEADERS, timeout: 15000 }
18
+ )
19
+
20
+ const data = res.data
21
+ const results = []
22
+
23
+ if (data?.AbstractText && data?.AbstractURL) {
24
+ results.push({
25
+ title: data.Heading || query,
26
+ url: data.AbstractURL,
27
+ snippet: data.AbstractText,
28
+ })
29
+ }
30
+
31
+ for (const item of (data?.RelatedTopics || [])) {
32
+ if (results.length >= limit) break
33
+ if (item?.Topics) {
34
+ for (const sub of item.Topics) {
35
+ if (results.length >= limit) break
36
+ if (sub?.FirstURL && sub?.Text) {
37
+ results.push({
38
+ title: sub.Text.split(' - ')[0] || sub.Text.slice(0, 60),
39
+ url: sub.FirstURL,
40
+ snippet: sub.Text,
41
+ })
42
+ }
43
+ }
44
+ } else if (item?.FirstURL && item?.Text) {
45
+ results.push({
46
+ title: item.Text.split(' - ')[0] || item.Text.slice(0, 60),
47
+ url: item.FirstURL,
48
+ snippet: item.Text,
49
+ })
50
+ }
51
+ }
52
+
53
+ if (!results.length) throw new Error('Sin resultados')
54
+ return results.slice(0, limit)
55
+ }
@@ -0,0 +1,12 @@
1
+ /*
2
+ * © Created by AxelDev09 🔥
3
+ * GitHub: https://github.com/AxelDev09
4
+ * Instagram: @axeldev09
5
+ * Deja los créditos we 🗣️
6
+ */
7
+
8
+ export { ytSearch } from './youtube.js'
9
+ export { googleSearch } from './google.js'
10
+ export { spotify } from './spotify.js'
11
+ export { giphy } from './giphy.js'
12
+ export { pinsearch, pinimg, pinvid } from './pinterest.js'
@@ -0,0 +1,162 @@
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
+ import * as cheerio from 'cheerio'
10
+
11
+ const HEADERS = {
12
+ 'User-Agent': 'Mozilla/5.0 (Linux; Android 10; SM-G975F) AppleWebKit/537.36 Chrome/120.0.0.0 Mobile Safari/537.36',
13
+ 'Accept-Language': 'en-US,en;q=0.9',
14
+ }
15
+
16
+ function toOriginal(src) {
17
+ return src.replace(/\/\d+x\//, '/originals/').replace(/\/\d+x\d+\//, '/originals/')
18
+ }
19
+
20
+ function isValidPin(src) {
21
+ return (
22
+ src.includes('i.pinimg.com') &&
23
+ /\.(jpg|jpeg|png|webp)/.test(src) &&
24
+ !src.includes('/60x60/') &&
25
+ !src.includes('/videos/thumbnails/')
26
+ )
27
+ }
28
+
29
+ function extractVidsFromHtml(html, limit) {
30
+ const seen = new Set()
31
+ const vids = []
32
+ const RE = /https:\/\/v\d+\.pinimg\.com\/videos\/[^\s"'\\]+\.mp4/g
33
+ for (const m of html.matchAll(RE)) {
34
+ const original = m[0]
35
+ if (seen.has(original)) continue
36
+ seen.add(original)
37
+ const hd = original.replace(/\/\d+p\//, '/720p/')
38
+ const sd = original.replace(/\/\d+p\//, '/480p/')
39
+ vids.push({ video: hd, sd, original })
40
+ if (vids.length >= limit) break
41
+ }
42
+ return vids
43
+ }
44
+
45
+ export async function pinvid(input, limit = 5) {
46
+ if (input.includes('pinterest.com/pin/') || input.includes('pin.it/')) {
47
+ let resolvedUrl = input
48
+ if (input.includes('pin.it/')) {
49
+ const r = await axios.get(input, { headers: HEADERS, maxRedirects: 5, timeout: 10000 })
50
+ resolvedUrl = r.request?.res?.responseUrl || r.config?.url || input
51
+ }
52
+ const res = await axios.get(resolvedUrl, { headers: HEADERS, timeout: 15000 })
53
+ const vids = extractVidsFromHtml(res.data, limit)
54
+ if (!vids.length) throw new Error('Este pin no tiene video')
55
+ return vids.map((v, i) => ({ index: i + 1, ...v }))
56
+ }
57
+
58
+ const res = await axios.get(
59
+ `https://www.pinterest.com/search/pins/?q=${encodeURIComponent(input + ' video')}`,
60
+ { headers: HEADERS, timeout: 15000 }
61
+ )
62
+ const vids = extractVidsFromHtml(res.data, limit)
63
+ if (!vids.length) throw new Error('Sin videos en Pinterest')
64
+ return vids.map((v, i) => ({ index: i + 1, ...v }))
65
+ }
66
+
67
+ export async function pinsearch(query, limit = 10) {
68
+ const res = await axios.get(
69
+ `https://www.pinterest.com/search/pins/?q=${encodeURIComponent(query)}`,
70
+ { headers: HEADERS, timeout: 15000 }
71
+ )
72
+ const $ = cheerio.load(res.data)
73
+ const seen = new Set()
74
+ const imgs = []
75
+
76
+ $('img').each((_, el) => {
77
+ const src = $(el).attr('src') || ''
78
+ const srcset = $(el).attr('srcset') || ''
79
+ const sources = [src, ...srcset.split(',').map(s => s.trim().split(' ')[0])]
80
+ for (const s of sources) {
81
+ if (!isValidPin(s)) continue
82
+ const high = toOriginal(s)
83
+ if (!seen.has(high)) { seen.add(high); imgs.push(high) }
84
+ }
85
+ })
86
+
87
+ $('[style]').each((_, el) => {
88
+ const style = $(el).attr('style') || ''
89
+ const m = style.match(/url\(['"]?(https:\/\/i\.pinimg\.com[^'")\s]+)['"]?\)/)
90
+ if (m && isValidPin(m[1])) {
91
+ const high = toOriginal(m[1])
92
+ if (!seen.has(high)) { seen.add(high); imgs.push(high) }
93
+ }
94
+ })
95
+
96
+ const result = imgs.slice(0, limit)
97
+ if (!result.length) throw new Error('Sin resultados en Pinterest')
98
+ return result.map((url, i) => ({ index: i + 1, image: url, url }))
99
+ }
100
+
101
+ export async function pinimg(input, limit = 5) {
102
+ if (!input.includes('pinterest.com/pin/') && !input.includes('pin.it/')) {
103
+ return pinsearch(input, limit)
104
+ }
105
+
106
+ let resolvedUrl = input
107
+ if (input.includes('pin.it/')) {
108
+ const r = await axios.get(input, { headers: HEADERS, maxRedirects: 5, timeout: 10000 })
109
+ resolvedUrl = r.request?.res?.responseUrl || r.config?.url || input
110
+ }
111
+ const url = resolvedUrl
112
+
113
+ const res = await axios.get(url, { headers: HEADERS, timeout: 15000 })
114
+ const html = res.data
115
+ const $ = cheerio.load(html)
116
+ const seen = new Set()
117
+ const imgs = []
118
+
119
+ const jsonMatch = html.match(/"orig"\s*:\s*\{"url"\s*:\s*"([^"]+)"/)
120
+ if (jsonMatch) {
121
+ const u = jsonMatch[1].replace(/\\u002F/g, '/').replace(/\\\//g, '/')
122
+ if (u.includes('i.pinimg.com')) { seen.add(u); imgs.push(u) }
123
+ }
124
+
125
+ const PIN_IMG_RE = /"url"\s*:\s*"(https:\\?\/\\?\/i\.pinimg\.com[^"]+)"/g
126
+ const pinMatches = [...html.matchAll(PIN_IMG_RE)]
127
+ for (const m of pinMatches) {
128
+ const u = m[1].replace(/\\u002F/g, '/').replace(/\\\//g, '/').replace(/\\/g, '')
129
+ if (isValidPin(u)) {
130
+ const high = toOriginal(u)
131
+ if (!seen.has(high)) { seen.add(high); imgs.push(high) }
132
+ }
133
+ }
134
+
135
+ $('img').each((_, el) => {
136
+ const src = $(el).attr('src') || ''
137
+ const srcset = $(el).attr('srcset') || ''
138
+ const sources = [src, ...srcset.split(',').map(s => s.trim().split(' ')[0])]
139
+ for (const s of sources) {
140
+ if (!isValidPin(s)) continue
141
+ const high = toOriginal(s)
142
+ if (!seen.has(high)) { seen.add(high); imgs.push(high) }
143
+ }
144
+ })
145
+
146
+ const ogImg = $('meta[property="og:image"]').attr('content') || ''
147
+ const ogTitle = $('meta[property="og:title"]').attr('content') || ''
148
+ const ogDesc = $('meta[property="og:description"]').attr('content') || ''
149
+ if (ogImg && !seen.has(ogImg)) imgs.push(ogImg)
150
+
151
+ const unique = [...new Set(imgs)].filter(Boolean)
152
+ if (!unique.length) throw new Error('No se pudo extraer la imagen del pin')
153
+
154
+ return {
155
+ id: resolvedUrl.match(/\/pin\/(\d+)/)?.[1] || '',
156
+ title: ogTitle,
157
+ description: ogDesc,
158
+ image: unique[0],
159
+ images: unique,
160
+ url: resolvedUrl,
161
+ }
162
+ }
@@ -0,0 +1,56 @@
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 HEADERS = {
11
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
12
+ }
13
+
14
+ async function searchTracks(query, limit = 5) {
15
+ const res = await axios.get(
16
+ `https://api.deezer.com/search?q=${encodeURIComponent(query)}&limit=${limit}`,
17
+ { headers: HEADERS, timeout: 15000 }
18
+ )
19
+ const tracks = res.data?.data || []
20
+ if (!tracks.length) throw new Error('Sin resultados')
21
+ return tracks.map(t => ({
22
+ type: 'track',
23
+ title: t.title,
24
+ artist: t.artist?.name || '',
25
+ album: t.album?.title || '',
26
+ duration: t.duration || 0,
27
+ thumbnail: t.album?.cover_big || t.album?.cover || '',
28
+ url: t.link || '',
29
+ preview: t.preview || null,
30
+ id: String(t.id),
31
+ }))
32
+ }
33
+
34
+ async function searchAlbums(query, limit = 5) {
35
+ const res = await axios.get(
36
+ `https://api.deezer.com/search/album?q=${encodeURIComponent(query)}&limit=${limit}`,
37
+ { headers: HEADERS, timeout: 15000 }
38
+ )
39
+ const albums = res.data?.data || []
40
+ if (!albums.length) throw new Error('Sin resultados')
41
+ return albums.map(a => ({
42
+ type: 'album',
43
+ title: a.title,
44
+ artist: a.artist?.name || '',
45
+ thumbnail: a.cover_big || a.cover || '',
46
+ url: a.link || '',
47
+ tracks: a.nb_tracks || 0,
48
+ year: a.release_date?.slice(0, 4) || '',
49
+ id: String(a.id),
50
+ }))
51
+ }
52
+
53
+ export async function spotify(query, type = 'track', limit = 5) {
54
+ if (type === 'album') return searchAlbums(query, limit)
55
+ return searchTracks(query, limit)
56
+ }
@@ -0,0 +1,15 @@
1
+ /*
2
+ * © Created by AxelDev09 🔥
3
+ * GitHub: https://github.com/AxelDev09
4
+ * Instagram: @axeldev09
5
+ * Deja los créditos we 🗣️
6
+ */
7
+
8
+ export { tiktokStalk } from './tiktokstalk.js'
9
+ export { lyricsSearch, lyricsGet } from './lyrics.js'
10
+ export { translate, getLangs } from './translator.js'
11
+ export { weather } from './weather.js'
12
+ export { qrGenerate, qrRead } from './qr.js'
13
+ export { shortenUrl, expandUrl } from './urlshortener.js'
14
+ export { news, newsCategories } from './news.js'
15
+ export { upload } from './upload.js'