Back to Home
Gistify Snippet
kyiov
1h ago·1 file(s)
Public

APKMody Android Apps & Games Downloader

Full-featured APKMody scraper to search modded Android apps & games, fetch version metadata, package names, cover images, and direct download links.

#apkmody#android#apk#downloader#mod#javascript
0
Raw
apkmody.jsjavascript
239 lines
1// apkmody.js — bundle standalone. npm deps: axios, cheerio, node:url
2
3/**
4 * Scraper: apkmody.js
5 * Source : mithra/apkmody.js
6 *
7 * Input : Function parameters / CLI args (e.g. query: string, url: string, options: object)
8 * Output : Promise<Object | Array | Buffer> (JSON formatted scraping response / stream URL / media metadata)
9 *
10 * CLI Run:
11 * node mithra/apkmody.js [functionName] [args...]
12 * node cli.js mithra/apkmody.js [functionName] [args...]
13 */
14
15import * as __ns_525 from 'axios'
16const axios = __ns_525.default
17import * as __ns_526 from 'cheerio'
18const cheerio = __ns_526
19import * as __ns_527 from 'node:url'
20const pathToFileURL = __ns_527.pathToFileURL
21
22/**
23 * APKMODY Scraper & Downloader
24 * Base: https://apkmody.mobi
25 * Author: phrzy
26 * Channel: https://whatsapp.com/channel/0029VbD1zGq6mYPUbtVh6U0L/121
27 */
28
29const BASE = 'https://apkmody.mobi'
30const SLUG_RE = /\/(games|apps)\/[^/]+/
31const SIZE_RE = /([\d.,]+)\s*(TB|GB|MB|KB)/i
32const SIZE_MULT = { b: 1, kb: 1024, mb: 1048576, gb: 1073741824, tb: 1099511627776 }
33
34const headers = {
35 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36',
36 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8',
37 'Accept-Language': 'id-ID,id;q=0.9,en-US;q=0.8,en;q=0.7',
38 'Referer': BASE + '/'
39}
40
41const client = axios.create({
42 timeout: 30000,
43 headers,
44 maxRedirects: 5,
45 validateStatus: (s) => s >= 200 && s < 400
46})
47
48const clean = (s) => (s ? s.replace(/\s+/g, ' ').trim() : '')
49
50function parseSize(text) {
51 const m = String(text || '').match(SIZE_RE)
52 if (!m) return null
53 const num = parseFloat(m[1].replace(/,/g, ''))
54 const unit = m[2].toLowerCase()
55 if (isNaN(num) || !SIZE_MULT[unit]) return null
56 return {
57 readable: clean(m[0]),
58 bytes: Math.round(num * SIZE_MULT[unit]),
59 number: num,
60 unit: unit.toUpperCase()
61 }
62}
63
64function packageFromIcon(url) {
65 const m = String(url || '').match(/topmongo\.com\/packages\/([^/]+)/)
66 return m ? m[1] : null
67}
68
69async function fetchPage(targetUrl) {
70 const fullUrl = targetUrl.startsWith('http') ? targetUrl : BASE + (targetUrl.startsWith('/') ? '' : '/') + targetUrl
71 const res = await client.get(fullUrl)
72 return {
73 url: res.request?.res?.responseUrl || fullUrl,
74 status: res.status,
75 body: res.data
76 }
77}
78
79function parseSearchItems($) {
80 const items = []
81 const seen = new Set()
82 $('article.card a[href]').each((_, el) => {
83 const m = ($(el).attr('href') || '').match(SLUG_RE)
84 if (!m) return
85 const url = BASE + m[0]
86 if (seen.has(url)) return
87 seen.add(url)
88 const cover = $(el).find('img').first().attr('src') || $(el).find('img').first().attr('data-src') || null
89 const title = clean($(el).find('.card-title .truncate').first().text())
90 const version = clean($(el).find('.card-excerpt').first().text())
91 if (title) {
92 items.push({ title, version, cover, url })
93 }
94 })
95 return items
96}
97
98function parseFiles($) {
99 const files = []
100 const seen = new Set()
101
102 // 1. Direct CDN / APK Links
103 $('a[href]').each((_, el) => {
104 const href = $(el).attr('href') || ''
105 const text = clean($(el).text())
106 const size = parseSize(text)
107
108 const isApk = href.endsWith('.apk') || href.includes('packages/') || href.includes('cdn.topmongo.com');
109 const isExcluded = href.includes('appstore.jooyfun.com') || href.includes('t.me') || href.includes('how-to');
110
111 if (isApk && !isExcluded && !seen.has(href)) {
112 seen.add(href)
113 files.push({
114 title: text || 'Download APK (Direct)',
115 url: href.startsWith('http') ? href : BASE + href,
116 size: size?.readable || 'Direct CDN Link'
117 })
118 }
119 })
120
121 // 2. Download page links if no direct CDN links
122 if (!files.length) {
123 $('a[href*="/download"]').each((_, el) => {
124 const href = $(el).attr('href') || ''
125 const text = clean($(el).text())
126 if (href && !seen.has(href) && !href.includes('appstore')) {
127 seen.add(href)
128 files.push({
129 title: text || 'Go to Download Page',
130 url: href.startsWith('http') ? href : BASE + href,
131 size: 'Web Download'
132 })
133 }
134 })
135 }
136
137 return files
138}
139
140function parseDetail($) {
141 const title = clean($('h1 strong, h1.entry-title, h1').first().text())
142 const version = clean($('.app-specs .spec-item:contains("Version") .spec-val, .has-small-font-size').first().text())
143 const mod = clean($('.app-specs .spec-item:contains("Mod") .spec-val, .has-accent-color').first().text())
144 const updated = clean($('.app-specs .spec-item:contains("Updated") .spec-val, time').first().text())
145 const icon = $('img[src*="topmongo.com/packages/"], .app-icon img').first().attr('src') || null
146 const desc = clean($('.entry-content p, .app-description, .post-content').first().text())
147
148 return {
149 title,
150 version,
151 mod: mod || 'Mod Info Available',
152 updated,
153 icon,
154 package: packageFromIcon(icon),
155 description: desc
156 }
157}
158
159async function search(query, page = 1) {
160 const q = String(query || '').trim()
161 if (!q) throw new Error('Query pencarian kosong')
162 const u = new URL(BASE + '/')
163 u.searchParams.set('s', q)
164 if (page && page > 1) u.searchParams.set('page', String(page))
165 const res = await fetchPage(u.toString())
166 const $ = cheerio.load(res.body)
167 const items = parseSearchItems($)
168 return {
169 query: q,
170 count: items.length,
171 page,
172 items
173 }
174}
175
176async function detail(url) {
177 const page = await fetchPage(url)
178 const $ = cheerio.load(page.body)
179 const parsed = parseDetail($)
180
181 // Ambil Download links
182 let files = parseFiles($)
183 if (!files.length) {
184 try {
185 const dlPage = await fetchPage(url.replace(/\/$/, '') + '/download')
186 files = parseFiles(cheerio.load(dlPage.body))
187 } catch {}
188 }
189
190 return {
191 ...parsed,
192 url: page.url,
193 downloads: files
194 }
195}
196
197const __dflt_9 = {
198 search,
199 detail,
200 fetchPage,
201 parseSize
202}
203
204export { search, detail, fetchPage, parseSize };
205export default __dflt_9;
206
207// ─────────────────────────────────────────────────────────────────────
208// Self-test: jalankan langsung dengan `node apkmody.js [args...]`
209// ─────────────────────────────────────────────────────────────────────
210if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
211 const __exports = { fetchPage: fetchPage, search: search, detail: detail, parseSize: parseSize, packageFromIcon: packageFromIcon, parseSearchItems: parseSearchItems, parseFiles: parseFiles, parseDetail: parseDetail, default: __dflt_9 }
212 const __fns = Object.fromEntries(Object.entries(__exports).filter(([, v]) => typeof v === 'function'))
213 const __names = Object.keys(__fns)
214 if (__names.length === 0) {
215 console.log('Tidak ada fungsi yang bisa di-demo. Import modul ini dari kode lain.')
216 } else {
217 const __search = __names.find((n) => /search|cari/i.test(n))
218 const __name = __search || __names[0]
219 const __fn = __fns[__name]
220 const __isClass = /^class\s/.test(Function.prototype.toString.call(__fn))
221 let __args = process.argv.slice(2).map((a) => { try { return JSON.parse(a) } catch { return a } })
222 if (__args.length === 0 && __search) __args.push('naruto')
223 if (__args.length === 0 && !__search && !__isClass && __fn.length > 0) {
224 console.log('Fungsi: ' + __names.join(', '))
225 console.log('Contoh: node ' + "apkmody.js" + ' ' + __name + ' <arg>')
226 } else {
227 ;(async () => {
228 try {
229 const r = __isClass ? new __fn(...__args) : await __fn(...__args)
230 console.log(JSON.stringify(r, null, 2))
231 } catch (e) {
232 console.error('Error: ' + (e && e.message ? e.message : e))
233 process.exitCode = 1
234 }
235 })()
236 }
237 }
238}
239
Discussion & Comments0

No comments yet — be the first to leave feedback or start a code discussion.

Sign in with your GitHub account to write comments and join the discussion.