Back to Home
Gistify Snippet
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
Rawapkmody.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 | |
| 15 | import * as __ns_525 from 'axios' |
| 16 | const axios = __ns_525.default |
| 17 | import * as __ns_526 from 'cheerio' |
| 18 | const cheerio = __ns_526 |
| 19 | import * as __ns_527 from 'node:url' |
| 20 | const 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 | |
| 29 | const BASE = 'https://apkmody.mobi' |
| 30 | const SLUG_RE = /\/(games|apps)\/[^/]+/ |
| 31 | const SIZE_RE = /([\d.,]+)\s*(TB|GB|MB|KB)/i |
| 32 | const SIZE_MULT = { b: 1, kb: 1024, mb: 1048576, gb: 1073741824, tb: 1099511627776 } |
| 33 | |
| 34 | const 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 | |
| 41 | const client = axios.create({ |
| 42 | timeout: 30000, |
| 43 | headers, |
| 44 | maxRedirects: 5, |
| 45 | validateStatus: (s) => s >= 200 && s < 400 |
| 46 | }) |
| 47 | |
| 48 | const clean = (s) => (s ? s.replace(/\s+/g, ' ').trim() : '') |
| 49 | |
| 50 | function 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 | |
| 64 | function packageFromIcon(url) { |
| 65 | const m = String(url || '').match(/topmongo\.com\/packages\/([^/]+)/) |
| 66 | return m ? m[1] : null |
| 67 | } |
| 68 | |
| 69 | async 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 | |
| 79 | function 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 | |
| 98 | function 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 | |
| 140 | function 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 | |
| 159 | async 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 | |
| 176 | async 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 | |
| 197 | const __dflt_9 = { |
| 198 | search, |
| 199 | detail, |
| 200 | fetchPage, |
| 201 | parseSize |
| 202 | } |
| 203 | |
| 204 | export { search, detail, fetchPage, parseSize }; |
| 205 | export default __dflt_9; |
| 206 | |
| 207 | // ───────────────────────────────────────────────────────────────────── |
| 208 | // Self-test: jalankan langsung dengan `node apkmody.js [args...]` |
| 209 | // ───────────────────────────────────────────────────────────────────── |
| 210 | if (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.