@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,201 @@
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_DESKTOP = {
11
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
12
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
13
+ 'Accept-Language': 'en-US,en;q=0.9',
14
+ 'Sec-Fetch-Mode': 'navigate',
15
+ }
16
+
17
+ const HEADERS_MOBILE = {
18
+ 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 16_0 like Mac OS X) AppleWebKit/605.1.15 Mobile/15E148',
19
+ 'Accept': 'text/html,application/xhtml+xml',
20
+ 'Accept-Language': 'en-US,en;q=0.9',
21
+ }
22
+
23
+ function cleanUrl(url) {
24
+ try {
25
+ const u = new URL(url)
26
+ if (u.hostname.includes('fb.watch')) return url
27
+ u.search = ''
28
+ return u.toString()
29
+ } catch { return url }
30
+ }
31
+
32
+ async function resolveShortUrl(url) {
33
+ if (!url.includes('fb.watch') && !url.includes('fb.com')) return url
34
+ const res = await axios.get(url, { headers: HEADERS_DESKTOP, maxRedirects: 5, timeout: 10000 })
35
+ return res.request?.res?.responseUrl || url
36
+ }
37
+
38
+ function extractVideoUrls(html) {
39
+ const results = { hd: null, sd: null, thumb: null, title: '' }
40
+
41
+ const unescape = s => s
42
+ .replace(/\\u0025/g, '%')
43
+ .replace(/\\u002F/g, '/')
44
+ .replace(/\\\//g, '/')
45
+ .replace(/\\"/g, '"')
46
+ .replace(/\\u0026/g, '&')
47
+ .replace(/\\u003C/g, '<')
48
+ .replace(/\\u003E/g, '>')
49
+
50
+ const hdPatterns = [
51
+ /"browser_native_hd_url"\s*:\s*"([^"]+)"/,
52
+ /"hd_src"\s*:\s*"([^"]+)"/,
53
+ /hd_src\s*:\s*"([^"]+)"/,
54
+ /"hdUrl"\s*:\s*"([^"]+)"/,
55
+ /"playable_url_quality_hd"\s*:\s*"([^"]+)"/,
56
+ /"videoUrl"\s*:\s*"([^"]+)"/,
57
+ /sd_src_no_ratelimit\s*:\s*"([^"]+)"/,
58
+ ]
59
+ for (const p of hdPatterns) {
60
+ const m = html.match(p)
61
+ if (m) { results.hd = unescape(m[1]); break }
62
+ }
63
+
64
+ const sdPatterns = [
65
+ /"browser_native_sd_url"\s*:\s*"([^"]+)"/,
66
+ /"sd_src"\s*:\s*"([^"]+)"/,
67
+ /sd_src\s*:\s*"([^"]+)"/,
68
+ /"sdUrl"\s*:\s*"([^"]+)"/,
69
+ /"playable_url"\s*:\s*"([^"]+)"/,
70
+ /"progressive_url"\s*:\s*"([^"]+)"/,
71
+ ]
72
+ for (const p of sdPatterns) {
73
+ const m = html.match(p)
74
+ if (m) { results.sd = unescape(m[1]); break }
75
+ }
76
+
77
+ const thumbPatterns = [
78
+ /"thumbnailImage"\s*:\s*\{"uri"\s*:\s*"([^"]+)"/,
79
+ /og:image"\s+content="([^"]+)"/,
80
+ /"preferred_thumbnail"\s*:\s*\{"image"\s*:\s*\{"uri"\s*:\s*"([^"]+)"/,
81
+ ]
82
+ for (const p of thumbPatterns) {
83
+ const m = html.match(p)
84
+ if (m) { results.thumb = unescape(m[1]); break }
85
+ }
86
+
87
+ const titlePatterns = [
88
+ /<title>([^<]+)<\/title>/,
89
+ /"story_name"\s*:\s*"([^"]+)"/,
90
+ /"title"\s*:\s*\{"text"\s*:\s*"([^"]+)"/,
91
+ ]
92
+ for (const p of titlePatterns) {
93
+ const m = html.match(p)
94
+ if (m) {
95
+ results.title = m[1].replace(/&#039;/g, "'").replace(/&amp;/g, '&')
96
+ break
97
+ }
98
+ }
99
+
100
+ return results
101
+ }
102
+
103
+ function extractVideoId(url) {
104
+ const patterns = [
105
+ /\/videos\/(\d+)/,
106
+ /\/video\/(\d+)/,
107
+ /v=(\d+)/,
108
+ /story_fbid=(\d+)/,
109
+ /\/reel\/(\d+)/,
110
+ /\/(\d{10,})/,
111
+ ]
112
+ for (const p of patterns) {
113
+ const m = url.match(p)
114
+ if (m) return m[1]
115
+ }
116
+ return null
117
+ }
118
+
119
+ async function tryMobilePage(url) {
120
+ const murl = url.replace('www.facebook.com', 'm.facebook.com')
121
+ const res = await axios.get(murl, { headers: HEADERS_MOBILE, timeout: 15000 })
122
+ return extractVideoUrls(res.data)
123
+ }
124
+
125
+ async function tryDesktopPage(url) {
126
+ const res = await axios.get(url, { headers: HEADERS_DESKTOP, timeout: 15000 })
127
+ return extractVideoUrls(res.data)
128
+ }
129
+
130
+ async function tryGraphQL(videoId) {
131
+ const queries = [
132
+ { doc_id: '10154874153461729', vars: { videoID: videoId } },
133
+ { doc_id: '6861055570608956', vars: { UFICommentID: videoId, UFIFeedbackID: videoId } },
134
+ { doc_id: '2443510449042541', vars: { videoID: videoId } },
135
+ ]
136
+
137
+ for (const q of queries) {
138
+ try {
139
+ const res = await axios.post(
140
+ 'https://www.facebook.com/api/graphql/',
141
+ new URLSearchParams({
142
+ variables: JSON.stringify(q.vars),
143
+ doc_id: q.doc_id,
144
+ server_timestamps: 'true',
145
+ }),
146
+ {
147
+ headers: {
148
+ ...HEADERS_DESKTOP,
149
+ 'Content-Type': 'application/x-www-form-urlencoded',
150
+ 'X-FB-Friendly-Name':'VideoPlayerQuery',
151
+ },
152
+ timeout: 15000,
153
+ }
154
+ )
155
+ const d = res.data
156
+ const node = d?.data?.video || d?.data?.node || d?.data?.mediaset
157
+ const hd = node?.playable_url_quality_hd || node?.browser_native_hd_url
158
+ const sd = node?.playable_url || node?.browser_native_sd_url
159
+ if (hd || sd) return { hd: hd || null, sd: sd || null, thumb: node?.thumbnailImage?.uri || '', title: node?.name || node?.title?.text || '' }
160
+ } catch {}
161
+ }
162
+ return null
163
+ }
164
+
165
+ export async function fbDownload(url) {
166
+ if (!url.includes('facebook.com') && !url.includes('fb.watch') && !url.includes('fb.com'))
167
+ throw new Error('URL de Facebook inválida')
168
+
169
+ const resolved = await resolveShortUrl(url)
170
+ const clean = cleanUrl(resolved)
171
+ const videoId = extractVideoId(clean)
172
+ const errors = []
173
+
174
+ try {
175
+ const parsed = await tryMobilePage(clean)
176
+ if (parsed.hd || parsed.sd) {
177
+ return { videoId: videoId || '', title: parsed.title, hd: parsed.hd, sd: parsed.sd, thumb: parsed.thumb, source: 'mobile' }
178
+ }
179
+ errors.push('mobile: sin urls')
180
+ } catch (e) { errors.push('mobile: ' + e.message) }
181
+
182
+ try {
183
+ const parsed = await tryDesktopPage(clean)
184
+ if (parsed.hd || parsed.sd) {
185
+ return { videoId: videoId || '', title: parsed.title, hd: parsed.hd, sd: parsed.sd, thumb: parsed.thumb, source: 'desktop' }
186
+ }
187
+ errors.push('desktop: sin urls')
188
+ } catch (e) { errors.push('desktop: ' + e.message) }
189
+
190
+ if (videoId) {
191
+ try {
192
+ const data = await tryGraphQL(videoId)
193
+ if (data?.hd || data?.sd) {
194
+ return { videoId, title: data.title, hd: data.hd, sd: data.sd, thumb: data.thumb, source: 'graphql' }
195
+ }
196
+ errors.push('graphql: sin urls')
197
+ } catch (e) { errors.push('graphql: ' + e.message) }
198
+ }
199
+
200
+ throw new Error('No se pudo descargar. Errores: ' + errors.join(' | '))
201
+ }
@@ -0,0 +1,156 @@
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
+ 'Accept': 'application/vnd.github.v3+json',
13
+ }
14
+
15
+ function parseRepo(input) {
16
+ const m = input.match(/github\.com\/([^/]+\/[^/]+)/)
17
+ if (m) return m[1].replace(/\.git$/, '')
18
+ if (/^[^/]+\/[^/]+$/.test(input)) return input
19
+ return null
20
+ }
21
+
22
+ async function get(url) {
23
+ const res = await axios.get(url, { headers: HEADERS, timeout: 15000 })
24
+ return res.data
25
+ }
26
+
27
+ export async function githubInfo(repo) {
28
+ const r = parseRepo(repo)
29
+ if (!r) throw new Error('Repo inválido. Usá owner/repo o URL de GitHub')
30
+
31
+ const data = await get(`https://api.github.com/repos/${r}`)
32
+
33
+ return {
34
+ name: data.name,
35
+ fullName: data.full_name,
36
+ description: data.description || '',
37
+ url: data.html_url,
38
+ stars: data.stargazers_count,
39
+ forks: data.forks_count,
40
+ watchers: data.watchers_count,
41
+ issues: data.open_issues_count,
42
+ language: data.language || '',
43
+ license: data.license?.name || '',
44
+ topics: data.topics || [],
45
+ private: data.private,
46
+ fork: data.fork,
47
+ createdAt: data.created_at,
48
+ updatedAt: data.updated_at,
49
+ pushedAt: data.pushed_at,
50
+ size: data.size,
51
+ defaultBranch: data.default_branch,
52
+ owner: {
53
+ login: data.owner?.login || '',
54
+ avatar: data.owner?.avatar_url || '',
55
+ url: data.owner?.html_url || '',
56
+ type: data.owner?.type || '',
57
+ },
58
+ cloneUrl: data.clone_url,
59
+ sshUrl: data.ssh_url,
60
+ zipUrl: `https://github.com/${r}/archive/refs/heads/${data.default_branch}.zip`,
61
+ }
62
+ }
63
+
64
+ export async function githubRelease(repo) {
65
+ const r = parseRepo(repo)
66
+ if (!r) throw new Error('Repo inválido')
67
+
68
+ const data = await get(`https://api.github.com/repos/${r}/releases/latest`)
69
+
70
+ return {
71
+ tag: data.tag_name,
72
+ name: data.name || data.tag_name,
73
+ body: data.body || '',
74
+ draft: data.draft,
75
+ prerelease: data.prerelease,
76
+ createdAt: data.created_at,
77
+ publishedAt: data.published_at,
78
+ url: data.html_url,
79
+ tarball: data.tarball_url,
80
+ zipball: data.zipball_url,
81
+ assets: (data.assets || []).map(a => ({
82
+ name: a.name,
83
+ size: a.size,
84
+ downloadUrl: a.browser_download_url,
85
+ downloads: a.download_count,
86
+ contentType: a.content_type,
87
+ })),
88
+ }
89
+ }
90
+
91
+ export async function githubContents(repo, path = '') {
92
+ const r = parseRepo(repo)
93
+ if (!r) throw new Error('Repo inválido')
94
+
95
+ const url = path
96
+ ? `https://api.github.com/repos/${r}/contents/${path}`
97
+ : `https://api.github.com/repos/${r}/contents`
98
+ const data = await get(url)
99
+
100
+ if (Array.isArray(data)) {
101
+ return data.map(f => ({
102
+ name: f.name,
103
+ path: f.path,
104
+ type: f.type,
105
+ size: f.size,
106
+ url: f.html_url,
107
+ download: f.download_url || null,
108
+ sha: f.sha,
109
+ }))
110
+ }
111
+
112
+ return {
113
+ name: data.name,
114
+ path: data.path,
115
+ type: data.type,
116
+ size: data.size,
117
+ url: data.html_url,
118
+ download: data.download_url,
119
+ content: data.encoding === 'base64' ? Buffer.from(data.content, 'base64').toString('utf-8') : data.content,
120
+ sha: data.sha,
121
+ }
122
+ }
123
+
124
+ export async function githubSearch(query, type = 'repositories', limit = 5) {
125
+ const validTypes = ['repositories', 'users', 'code', 'issues']
126
+ if (!validTypes.includes(type)) throw new Error('Tipo inválido. Usá: ' + validTypes.join(', '))
127
+
128
+ const data = await get(
129
+ `https://api.github.com/search/${type}?q=${encodeURIComponent(query)}&per_page=${limit}`
130
+ )
131
+
132
+ const items = data.items || []
133
+
134
+ if (type === 'repositories') {
135
+ return items.map(r => ({
136
+ name: r.full_name,
137
+ description: r.description || '',
138
+ url: r.html_url,
139
+ stars: r.stargazers_count,
140
+ forks: r.forks_count,
141
+ language: r.language || '',
142
+ updatedAt: r.updated_at,
143
+ }))
144
+ }
145
+
146
+ if (type === 'users') {
147
+ return items.map(u => ({
148
+ login: u.login,
149
+ url: u.html_url,
150
+ avatar: u.avatar_url,
151
+ type: u.type,
152
+ }))
153
+ }
154
+
155
+ return items.slice(0, limit)
156
+ }
@@ -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 { ytInfo, ytDownload, ytSearch } from './youtube.js'
9
+ export { ytInfoV2, ytDownloadV2, getFileSizeV2 } from './youtubev2.js'
10
+ export { tiktokInfo, tiktokDownload } from './tiktok.js'
11
+ export { fbDownload } from './facebook.js'
12
+ export { tweetInfo, tweetDownload } from './twitter.js'
13
+ export { mediafireInfo } from './mediafire.js'
14
+ export { githubInfo, githubRelease, githubContents, githubSearch } from './github.js'
15
+ export { apkSearch, apkInfo } from './apk.js'
@@ -0,0 +1,77 @@
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 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
13
+ }
14
+
15
+ function parseKey(url) {
16
+ const m = url.match(/mediafire\.com\/file\/([a-z0-9]+)/)
17
+ return m ? m[1] : null
18
+ }
19
+
20
+ function deduplicateName(raw) {
21
+ const clean = raw.trim().replace(/\s+/g, ' ')
22
+ const half = Math.ceil(clean.length / 2)
23
+ const first = clean.slice(0, half)
24
+ const second = clean.slice(half).trim()
25
+ return second.startsWith(first.trim()) ? first.trim() : clean
26
+ }
27
+
28
+ async function tryPage(url) {
29
+ const res = await axios.get(url, { headers: HEADERS, timeout: 15000 })
30
+ const $ = cheerio.load(res.data)
31
+ const link = $('a#downloadButton').attr('href') || $('a.input').attr('href')
32
+ if (!link) throw new Error('page: sin link de descarga')
33
+ const name = deduplicateName($('div.filename').text())
34
+ const size = $('ul.details li').first().text().replace('File size:', '').trim()
35
+ return { link, name, size }
36
+ }
37
+
38
+ async function tryAPI(key) {
39
+ const res = await axios.get(
40
+ `https://www.mediafire.com/api/1.5/file/get_links.php?quick_key=${key}&link_type=normal_download&response_format=json`,
41
+ { headers: HEADERS, timeout: 15000 }
42
+ )
43
+ const data = res.data?.response
44
+ if (data?.result !== 'Success') throw new Error('api: ' + (data?.message || 'sin resultado'))
45
+ const dl = data?.links?.[0]?.normal_download
46
+ if (!dl) throw new Error('api: sin download link')
47
+ return dl
48
+ }
49
+
50
+ export async function mediafireInfo(url) {
51
+ if (!url.includes('mediafire.com')) throw new Error('URL de MediaFire inválida')
52
+
53
+ const key = parseKey(url)
54
+ const errors = []
55
+ let info = null
56
+
57
+ try {
58
+ info = await tryPage(url)
59
+ } catch (e) { errors.push('page: ' + e.message) }
60
+
61
+ if (!info?.link && key) {
62
+ try {
63
+ const link = await tryAPI(key)
64
+ info = { ...(info || {}), link }
65
+ } catch (e) { errors.push('api: ' + e.message) }
66
+ }
67
+
68
+ if (!info?.link) throw new Error('No se pudo obtener el link. Errores: ' + errors.join(' | '))
69
+
70
+ return {
71
+ key: key || '',
72
+ name: info.name || '',
73
+ size: info.size || '',
74
+ download: info.link,
75
+ url,
76
+ }
77
+ }
@@ -0,0 +1,58 @@
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 resolveUrl(url) {
15
+ if (url.includes('vm.tiktok.com') || url.includes('vt.tiktok.com')) {
16
+ const res = await axios.get(url, { headers: HEADERS, maxRedirects: 5, timeout: 10000 })
17
+ return res.request?.res?.responseUrl || res.config?.url || url
18
+ }
19
+ return url
20
+ }
21
+
22
+ export async function tiktokDownload(url) {
23
+ const resolved = await resolveUrl(url)
24
+ const res = await axios.get(
25
+ `https://tikwm.com/api/?url=${encodeURIComponent(resolved)}`,
26
+ { headers: HEADERS, timeout: 20000 }
27
+ )
28
+
29
+ const data = res.data
30
+ if (data?.code !== 0 || !data?.data) throw new Error('tikwm: ' + (data?.msg || 'sin datos'))
31
+
32
+ const d = data.data
33
+ return {
34
+ id: d.id || '',
35
+ title: d.title || '',
36
+ author: d.author?.nickname || d.author?.unique_id || '',
37
+ thumbnail: d.cover || d.origin_cover || '',
38
+ duration: d.duration || 0,
39
+ nowatermark: d.play || null,
40
+ watermark: d.wmplay || null,
41
+ audio: d.music || null,
42
+ music: {
43
+ title: d.music_info?.title || '',
44
+ author: d.music_info?.author || '',
45
+ url: d.music_info?.play || null,
46
+ cover: d.music_info?.cover || '',
47
+ },
48
+ plays: d.play_count || 0,
49
+ likes: d.digg_count || 0,
50
+ comments: d.comment_count || 0,
51
+ shares: d.share_count || 0,
52
+ source: 'tikwm',
53
+ }
54
+ }
55
+
56
+ export async function tiktokInfo(url) {
57
+ return tiktokDownload(url)
58
+ }
@@ -0,0 +1,106 @@
1
+
2
+ /*
3
+ * © Created by AxelDev09 🔥
4
+ * GitHub: https://github.com/AxelDev09
5
+ * Instagram: @axeldev09
6
+ * Deja los créditos we 🗣️
7
+ */
8
+
9
+ import axios from 'axios'
10
+
11
+ const HEADERS = {
12
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 Chrome/120.0.0.0 Safari/537.36',
13
+ 'Referer': 'https://platform.twitter.com/',
14
+ }
15
+
16
+ function parseTweetId(url) {
17
+ const m = url.match(/\/status\/(\d+)/)
18
+ return m ? m[1] : (/^\d+$/.test(url) ? url : null)
19
+ }
20
+
21
+ function parseMedia(details) {
22
+ if (!details?.length) return []
23
+ return details.map(m => {
24
+ if (m.type === 'video' || m.type === 'animated_gif') {
25
+ const variants = m.video_info?.variants || []
26
+ const best = variants
27
+ .filter(v => v.content_type === 'video/mp4')
28
+ .sort((a, b) => (b.bitrate || 0) - (a.bitrate || 0))[0]
29
+ return {
30
+ type: m.type === 'animated_gif' ? 'gif' : 'video',
31
+ url: best?.url || '',
32
+ thumbnail: m.media_url_https + '?format=jpg&name=large',
33
+ width: m.original_info?.width || 0,
34
+ height: m.original_info?.height || 0,
35
+ variants: variants.filter(v => v.url).map(v => ({
36
+ url: v.url,
37
+ contentType: v.content_type,
38
+ bitrate: v.bitrate || 0,
39
+ })),
40
+ }
41
+ }
42
+ return {
43
+ type: 'photo',
44
+ url: m.media_url_https + '?format=jpg&name=orig',
45
+ thumb: m.media_url_https + '?format=jpg&name=small',
46
+ width: m.original_info?.width || 0,
47
+ height: m.original_info?.height || 0,
48
+ }
49
+ })
50
+ }
51
+
52
+ export async function tweetInfo(url) {
53
+ const id = parseTweetId(url)
54
+ if (!id) throw new Error('URL/ID de Twitter inválido')
55
+
56
+ const res = await axios.get(
57
+ `https://cdn.syndication.twimg.com/tweet-result?id=${id}&lang=en&features=tfw_timeline_list%3A%3Btfw_follower_count_sunset%3Atrue&token=abc123`,
58
+ { headers: HEADERS, timeout: 15000 }
59
+ )
60
+
61
+ const d = res.data
62
+ if (!d?.id_str) throw new Error('Tweet no encontrado o privado')
63
+
64
+ const medias = parseMedia(d.mediaDetails)
65
+
66
+ return {
67
+ id: d.id_str,
68
+ text: d.text || '',
69
+ lang: d.lang || '',
70
+ createdAt: d.created_at || '',
71
+ likes: d.favorite_count || 0,
72
+ replies: d.conversation_count || 0,
73
+ author: {
74
+ id: d.user?.id_str || '',
75
+ name: d.user?.name || '',
76
+ username: d.user?.screen_name || '',
77
+ avatar: d.user?.profile_image_url_https?.replace('_normal', '') || '',
78
+ verified: d.user?.is_blue_verified || false,
79
+ },
80
+ hashtags: d.entities?.hashtags?.map(h => h.text) || [],
81
+ mentions: d.entities?.user_mentions?.map(m => m.screen_name) || [],
82
+ urls: d.entities?.urls?.map(u => u.expanded_url) || [],
83
+ medias,
84
+ url: `https://x.com/${d.user?.screen_name}/status/${d.id_str}`,
85
+ }
86
+ }
87
+
88
+ export async function tweetDownload(url) {
89
+ const info = await tweetInfo(url)
90
+ const videos = info.medias.filter(m => m.type === 'video' || m.type === 'gif')
91
+ const photos = info.medias.filter(m => m.type === 'photo')
92
+
93
+ if (!videos.length && !photos.length)
94
+ throw new Error('Este tweet no tiene media descargable')
95
+
96
+ return {
97
+ id: info.id,
98
+ text: info.text,
99
+ author: info.author,
100
+ createdAt: info.createdAt,
101
+ videos,
102
+ photos,
103
+ hasVideo: videos.length > 0,
104
+ hasPhoto: photos.length > 0,
105
+ }
106
+ }