Back to Home
Gistify Snippet
Public
Scraper animeindo
Scraper animeindo
#github-gist#javascript#gist:a6480100e68ba991e6b7d021dc6831aa#gistsync:2026-09-02T12:40:33Z
1
Rawanimeindo.jsjavascript
240 lines| 1 | |
| 2 | |
| 3 | import * as __ns_513 from 'cheerio' |
| 4 | const cheerio = __ns_513 |
| 5 | import * as __ns_514 from 'node:url' |
| 6 | const pathToFileURL = __ns_514.pathToFileURL |
| 7 | |
| 8 | const BASE = 'https://anime-indo.lol' |
| 9 | const UA = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0.0.0 Safari/537.36' |
| 10 | |
| 11 | async function get(url, referer = BASE + '/') { |
| 12 | const r = await fetch(url, { |
| 13 | headers: { |
| 14 | 'User-Agent': UA, |
| 15 | Accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8', |
| 16 | 'Accept-Language': 'id-ID,id;q=0.9,en;q=0.8', |
| 17 | Referer: referer, |
| 18 | }, |
| 19 | redirect: 'follow', |
| 20 | signal: AbortSignal.timeout(25000), |
| 21 | }) |
| 22 | const body = await r.text() |
| 23 | if (r.status !== 200) throw new Error(`HTTP ${r.status} untuk ${url}`) |
| 24 | return body |
| 25 | } |
| 26 | |
| 27 | function toAbs(u) { |
| 28 | if (!u) return '' |
| 29 | if (u.startsWith('http')) return u |
| 30 | return BASE + (u.startsWith('/') ? u : '/' + u) |
| 31 | } |
| 32 | |
| 33 | async function searchAnime(query) { |
| 34 | const q = String(query || '').trim() |
| 35 | if (!q) return [] |
| 36 | const seen = new Set() |
| 37 | const results = [] |
| 38 | |
| 39 | try { |
| 40 | const html = await get(`${BASE}/search.php?q=${encodeURIComponent(q)}`) |
| 41 | const $ = cheerio.load(html) |
| 42 | $('a[href*="/anime/"]').each((i, el) => { |
| 43 | const href = $(el).attr('href') || '' |
| 44 | const title = $(el).text().trim().replace(/\s+/g, ' ') |
| 45 | if (!href || seen.has(href)) return |
| 46 | seen.add(href) |
| 47 | results.push({ |
| 48 | source: 'animeindo', |
| 49 | title: title || href.replace(/^\/anime\//, '').replace(/\/$/, '').replace(/-/g, ' '), |
| 50 | url: toAbs(href), |
| 51 | image: '', |
| 52 | type: '', |
| 53 | }) |
| 54 | }) |
| 55 | } catch {} |
| 56 | |
| 57 | if (!results.length) { |
| 58 | try { |
| 59 | const html = await get(`${BASE}/anime-list/`) |
| 60 | const $ = cheerio.load(html) |
| 61 | const ql = q.toLowerCase() |
| 62 | const tokens = ql.split(/[^a-z0-9]+/).filter(t => t.length > 1) |
| 63 | const scored = [] |
| 64 | $('a[href*="/anime/"]').each((i, el) => { |
| 65 | const href = $(el).attr('href') || '' |
| 66 | const title = $(el).text().trim().replace(/\s+/g, ' ') |
| 67 | if (!href || seen.has(href)) return |
| 68 | const hay = (title + ' ' + href).toLowerCase() |
| 69 | const hits = tokens.filter(t => hay.includes(t)).length |
| 70 | if (!hits) return |
| 71 | seen.add(href) |
| 72 | scored.push({ |
| 73 | score: hits, |
| 74 | item: { |
| 75 | source: 'animeindo', |
| 76 | title: title || href.replace(/^\/anime\//, '').replace(/\/$/, '').replace(/-/g, ' '), |
| 77 | url: toAbs(href), |
| 78 | image: '', |
| 79 | type: '', |
| 80 | }, |
| 81 | }) |
| 82 | }) |
| 83 | scored.sort((a, b) => b.score - a.score) |
| 84 | scored.forEach(s => results.push(s.item)) |
| 85 | } catch {} |
| 86 | } else { |
| 87 | const ql = q.toLowerCase() |
| 88 | const tokens = ql.split(/[^a-z0-9]+/).filter(t => t.length > 1) |
| 89 | results.sort((a, b) => { |
| 90 | const sa = tokens.filter(t => a.title.toLowerCase().includes(t)).length |
| 91 | const sb = tokens.filter(t => b.title.toLowerCase().includes(t)).length |
| 92 | return sb - sa |
| 93 | }) |
| 94 | } |
| 95 | |
| 96 | return results |
| 97 | } |
| 98 | |
| 99 | async function getAnimeDetail(url) { |
| 100 | const html = await get(url) |
| 101 | const $ = cheerio.load(html) |
| 102 | |
| 103 | const title = $('h1.entry-title, h1').first().text().trim() || $('title').text().replace(/ - Animeindo.*$/i, '').trim() |
| 104 | const img = $('.animeinfo img, .poster img, .thumb img').first().attr('src') || '' |
| 105 | const genres = [] |
| 106 | $('a[href*="/genre/"]').each((i, el) => { |
| 107 | const t = $(el).text().trim() |
| 108 | if (t && !genres.includes(t)) genres.push(t) |
| 109 | }) |
| 110 | |
| 111 | const episodes = [] |
| 112 | $('div.ep a[href*="episode-"]').each((i, el) => { |
| 113 | const href = $(el).attr('href') || '' |
| 114 | const m = href.match(/episode-(\d+)/) |
| 115 | if (!m) return |
| 116 | episodes.push({ |
| 117 | number: parseInt(m[1], 10), |
| 118 | title: `Episode ${m[1]}`, |
| 119 | url: toAbs(href), |
| 120 | date: '', |
| 121 | }) |
| 122 | }) |
| 123 | episodes.sort((a, b) => a.number - b.number) |
| 124 | |
| 125 | return { source: 'animeindo', title, image: img, genres, synopsis: '', episodes } |
| 126 | } |
| 127 | |
| 128 | async function getEpisode(url) { |
| 129 | const html = await get(url, url) |
| 130 | const $ = cheerio.load(html) |
| 131 | |
| 132 | const title = $('h1.entry-title, h1').first().text().trim() || $('title').text().replace(/ - Animeindo.*$/i, '').trim() |
| 133 | |
| 134 | let iframe = '' |
| 135 | $('iframe').each((i, el) => { |
| 136 | const src = $(el).attr('src') || $(el).attr('data-src') || '' |
| 137 | if (src) { iframe = src; return false } |
| 138 | }) |
| 139 | |
| 140 | const downloadLinks = [] |
| 141 | const seenDl = new Set() |
| 142 | $('a').each((i, el) => { |
| 143 | const href = $(el).attr('href') || '' |
| 144 | const txt = $(el).text().trim().replace(/\s+/g, ' ').slice(0, 60) |
| 145 | if (!href || href === '#' || href.startsWith(BASE) || /^(javascript|mailto):/i.test(href)) return |
| 146 | if (seenDl.has(href)) return |
| 147 | seenDl.add(href) |
| 148 | downloadLinks.push({ label: txt || 'link', url: href }) |
| 149 | }) |
| 150 | |
| 151 | return { source: 'animeindo', title, iframe: toAbs(iframe), downloadLinks } |
| 152 | } |
| 153 | |
| 154 | async function resolveVideo(iframeUrl) { |
| 155 | if (!iframeUrl) return null |
| 156 | if (/\/(?:mp4|webm|mkv)(?:\?|$)/i.test(iframeUrl)) return iframeUrl |
| 157 | if (/m3u8/i.test(iframeUrl)) return iframeUrl |
| 158 | |
| 159 | if (iframeUrl.includes('btube3.php')) { |
| 160 | try { |
| 161 | const body = await get(iframeUrl, BASE + '/') |
| 162 | const m = body.match(/https?:\\?\/\\?\/[^"'\s<>\\]+googlevideo\.com\/videoplayback[^"'\s<>\\]*/i) |
| 163 | if (m) { |
| 164 | return m[0] |
| 165 | .replace(/\\u002f/gi, '/') |
| 166 | .replace(/\\u0026/gi, '&') |
| 167 | .replace(/\\\//g, '/') |
| 168 | .replace(/\\/g, '') |
| 169 | } |
| 170 | const alt = body.match(/https?:\\?\/\\?\/[^"'\s<>\\]+(?:\.m3u8|\.mp4)[^"'\s<>\\]*/i) |
| 171 | if (alt) return alt[0].replace(/\\u002f/gi, '/').replace(/\\u0026/gi, '&').replace(/\\\//g, '/') |
| 172 | return null |
| 173 | } catch { |
| 174 | return null |
| 175 | } |
| 176 | } |
| 177 | |
| 178 | if (/hls\.php|xtwap/i.test(iframeUrl)) { |
| 179 | try { |
| 180 | const body = await get(iframeUrl, BASE + '/') |
| 181 | const m = body.match(/https?:\\?\/\\?\/[^"'\s<>\\]+\.m3u8[^"'\s<>\\]*/i) |
| 182 | if (m) { |
| 183 | return m[0] |
| 184 | .replace(/\\u002f/gi, '/') |
| 185 | .replace(/\\u0026/gi, '&') |
| 186 | .replace(/\\\//g, '/') |
| 187 | .replace(/\\/g, '') |
| 188 | } |
| 189 | return null |
| 190 | } catch { |
| 191 | return null |
| 192 | } |
| 193 | } |
| 194 | |
| 195 | return iframeUrl |
| 196 | } |
| 197 | |
| 198 | const __dflt_7 = { searchAnime, getAnimeDetail, getEpisode, resolveVideo } |
| 199 | |
| 200 | if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { |
| 201 | const __exports = { searchAnime: searchAnime, getAnimeDetail: getAnimeDetail, getEpisode: getEpisode, resolveVideo: resolveVideo, default: __dflt_7 } |
| 202 | const __fns = Object.fromEntries(Object.entries(__exports).filter(([, v]) => typeof v === 'function')) |
| 203 | const __names = Object.keys(__fns) |
| 204 | if (__names.length === 0) { |
| 205 | console.log('Tidak ada fungsi yang bisa di-demo. Import modul ini dari kode lain.') |
| 206 | } else { |
| 207 | const __search = __names.find((n) => /search|cari/i.test(n)) |
| 208 | const __name = __search || __names[0] |
| 209 | const __fn = __fns[__name] |
| 210 | const __isClass = /^class\s/.test(Function.prototype.toString.call(__fn)) |
| 211 | const __defaultArgs = (() => { |
| 212 | const n = __name.toLowerCase() |
| 213 | if (/emoji/.test(n)) return [{ emoji: '😀😃' }] |
| 214 | if (/textpro|generate.*text|effect/.test(n)) return ['neon', 'Hello'] |
| 215 | if (/image|wallpaper|search|query|anime|movie|manga|news|lyrics|wiki|quote|stalk|detail/.test(n)) return ['naruto'] |
| 216 | if (/download|youtube|video|audio|music|spotify|tiktok|instagram|facebook|twitter|reels|terabox|media/.test(n)) return ['https://www.youtube.com/watch?v=dQw4w9WgXcQ'] |
| 217 | if (/dork/.test(n)) return ['example.com', 'documents'] |
| 218 | if (/tts|speech|prompt|chat|ask|translate/.test(n)) return ['Hello world'] |
| 219 | if (/parse|format|normalize|list|features/.test(n)) return [] |
| 220 | return ['test'] |
| 221 | })() |
| 222 | let __args = __defaultArgs |
| 223 | if (process.argv.length > 2) __args = process.argv.slice(2).map((a) => { try { return JSON.parse(a) } catch { return a } }) |
| 224 | if (__args.length === 0 && !__search && !__isClass && __fn.length > 0) { |
| 225 | console.log('Fungsi: ' + __names.join(', ')) |
| 226 | console.log('Contoh: node ' + "animeindo.js" + ' ' + __name + ' <arg>') |
| 227 | } else { |
| 228 | ;(async () => { |
| 229 | try { |
| 230 | const r = __isClass ? new __fn(...__args) : await __fn(...__args) |
| 231 | console.log(JSON.stringify(r, null, 2)) |
| 232 | } catch (e) { |
| 233 | console.error('Error: ' + (e && e.message ? e.message : e)) |
| 234 | process.exitCode = 1 |
| 235 | } |
| 236 | })() |
| 237 | } |
| 238 | } |
| 239 | } |
| 240 | |
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.